├── .prettierignore
├── .prettierrc
├── .stylelintrc
├── docs
├── loading.md
├── faq.md
├── has-number.md
├── basic.md
├── scroll-duration.md
└── auto-scroll.md
├── .gitignore
├── src
├── components
│ ├── line-number.vue
│ ├── loading.vue
│ ├── line-wrapper.vue
│ └── line-content.vue
├── utils
│ ├── index.js
│ └── ansi-parse.js
├── index.js
├── log-viewer.d.ts
└── log-viewer.vue
├── .babelrc.js
├── .editorconfig
├── .github
└── badge.yml
├── netlify.sh
├── .travis.yml
├── .all-contributorsrc
├── test
└── utils
│ ├── index.test.js
│ └── ansi-parse.test.js
├── LICENSE
├── notify.sh
├── styleguide.config.js
├── package.json
├── README-zh.md
└── README.md
/.prettierignore:
--------------------------------------------------------------------------------
1 | docs
2 | dist
3 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | semi: false
2 | singleQuote: true
3 | bracketSpacing: false
4 |
--------------------------------------------------------------------------------
/.stylelintrc:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "stylelint-config-standard",
3 | "rules": {
4 | "no-empty-source": null
5 | }
6 | }
7 |
--------------------------------------------------------------------------------
/docs/loading.md:
--------------------------------------------------------------------------------
1 | 显示 loading
2 |
3 | set loading true to show loading
4 |
5 | ```vue
6 |
7 |
8 |
9 | ```
10 |
11 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules/
3 | npm-debug.log*
4 | yarn-debug.log*
5 | yarn-error.log*
6 | dist
7 | docs/build
8 | docs/index.html
9 | docs/*.woff
10 | docs/*.ttf
11 |
12 | # Editor directories and files
13 | .idea
14 | .vscode
15 | *.suo
16 | *.ntvs*
17 | *.njsproj
18 | *.sln
19 | .env
20 |
--------------------------------------------------------------------------------
/docs/faq.md:
--------------------------------------------------------------------------------
1 | ## 在 TypeScript 中指定组件的类型
2 |
3 | ```html
4 |
13 | ```
14 |
--------------------------------------------------------------------------------
/src/components/line-number.vue:
--------------------------------------------------------------------------------
1 |
2 | {{ number }}
3 |
4 |
13 |
--------------------------------------------------------------------------------
/.babelrc.js:
--------------------------------------------------------------------------------
1 | module.exports = api => {
2 | return {
3 | presets: [['@babel/env', {modules: api.env('test') ? 'commonjs' : false}]],
4 | plugins: [
5 | [
6 | '@babel/transform-runtime',
7 | {
8 | regenerator: true
9 | }
10 | ]
11 | ]
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | end_of_line = lf
7 | indent_style = space
8 | indent_size = 2
9 | insert_final_newline = true
10 | trim_trailing_whitespace = true
11 |
12 | [*.md]
13 | insert_final_newline = false
14 | trim_trailing_whitespace = false
15 |
--------------------------------------------------------------------------------
/.github/badge.yml:
--------------------------------------------------------------------------------
1 | types:
2 | feat: 'enhancement'
3 | fix:
4 | hack: 'hack'
5 | default: 'bug'
6 | hack: 'hack'
7 | docs: 'documentation'
8 | refactor: 'refactor'
9 | style: 'style'
10 | test: 'test'
11 | perf: 'performance'
12 | chore:
13 | deps: 'dependencies'
14 | default: 'chore'
15 |
--------------------------------------------------------------------------------
/netlify.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | echo "is netlify: $NETLIFY"
3 | echo "in branch: $BRANCH"
4 | echo "head: $HEAD"
5 |
6 | if [ "$NETLIFY" != "true" ]
7 | then
8 | echo "this script only runs in netlify, bye"
9 | exit 1
10 | fi
11 |
12 | if [ "$BRANCH" != "dev" ] && [ "$HEAD" != "dev" ]
13 | then
14 | yarn doc
15 | else
16 | echo "this script only runs in targeting dev's PR deploy preview, bye"
17 | fi
18 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | branches:
2 | only:
3 | - master
4 | language: node_js
5 | node_js:
6 | - lts/*
7 | git:
8 | depth: 30
9 | install:
10 | - yarn --frozen-lockfile
11 | - yarn test
12 | script:
13 | - ./build.sh
14 | after_script:
15 | - ./notify.sh
16 | cache: yarn
17 | deploy:
18 | - provider: pages
19 | local-dir: docs
20 | github-token: $GITHUB_TOKEN
21 | skip-cleanup: true
22 | keep-history: true
23 | - provider: npm
24 | email: levy9527@qq.com
25 | api_key: $NPM_TOKEN
26 | skip-cleanup: true
27 |
--------------------------------------------------------------------------------
/docs/has-number.md:
--------------------------------------------------------------------------------
1 | 是否显示行号
2 |
3 | set has-number true to show line number
4 |
5 | ```vue
6 |
7 |
8 |
9 |
10 |
11 |
21 |
26 | ```
27 |
28 |
--------------------------------------------------------------------------------
/src/utils/index.js:
--------------------------------------------------------------------------------
1 | import ansiParse from './ansi-parse'
2 |
3 | const ENCODED_NEWLINE = /\r{0,1}\n(?!\u0008)/
4 |
5 | // RegExp reference:
6 | // http://jafrog.com/2013/11/23/colors-in-terminal.html
7 | // https://en.wikipedia.org/wiki/ANSI_escape_code
8 |
9 | export const split2Lines = str => str.split(ENCODED_NEWLINE)
10 |
11 | export default log => {
12 | const stringLines = split2Lines(log)
13 | const stringLinesText = []
14 | stringLines.forEach(line => {
15 | if (!line) {
16 | return
17 | }
18 | stringLinesText.push(ansiParse(line))
19 | })
20 | return stringLinesText
21 | }
22 |
--------------------------------------------------------------------------------
/docs/basic.md:
--------------------------------------------------------------------------------
1 | basic usage
2 |
3 | ```vue
4 |
5 |
6 |
7 |
8 |
35 |
36 | ```
37 |
--------------------------------------------------------------------------------
/.all-contributorsrc:
--------------------------------------------------------------------------------
1 | {
2 | "projectName": "log-viewer",
3 | "projectOwner": "FEMessage",
4 | "repoType": "github",
5 | "repoHost": "https://github.com",
6 | "files": [
7 | "README.md"
8 | ],
9 | "imageSize": 100,
10 | "commit": false,
11 | "commitConvention": "angular",
12 | "contributors": [
13 | {
14 | "login": "snowlocked",
15 | "name": "snowlocked",
16 | "avatar_url": "https://avatars0.githubusercontent.com/u/19562649?v=4",
17 | "profile": "https://github.com/snowlocked",
18 | "contributions": [
19 | "code",
20 | "doc",
21 | "test"
22 | ]
23 | },
24 | {
25 | "login": "evillt",
26 | "name": "EVILLT",
27 | "avatar_url": "https://avatars3.githubusercontent.com/u/19513289?v=4",
28 | "profile": "https://evila.me",
29 | "contributions": [
30 | "code"
31 | ]
32 | }
33 | ],
34 | "contributorsPerLine": 7
35 | }
36 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | // Import vue component
2 | import Component from './log-viewer.vue'
3 |
4 | // `Vue.use` automatically prevents you from using
5 | // the same plugin more than once,
6 | // so calling it multiple times on the same plugin
7 | // will install the plugin only once
8 | Component.install = Vue => {
9 | Vue.component(Component.name, Component)
10 | }
11 |
12 | // To auto-install when vue is found
13 | let GlobalVue = null
14 | if (typeof window !== 'undefined') {
15 | GlobalVue = window.Vue
16 | } else if (typeof global !== 'undefined') {
17 | GlobalVue = global.Vue
18 | }
19 | if (GlobalVue) {
20 | GlobalVue.use(Component)
21 | }
22 |
23 | // To allow use as module (npm/webpack/etc.) export component
24 | export default Component
25 |
26 | // It's possible to expose named exports when writing components that can
27 | // also be used as directives, etc. - eg. import { RollupDemoDirective } from 'rollup-demo';
28 | // export const RollupDemoDirective = component;
29 |
--------------------------------------------------------------------------------
/docs/scroll-duration.md:
--------------------------------------------------------------------------------
1 | scroll-duration
2 |
3 | When auto-scroll is true, you can set scroll-duration to control how long(ms) scroll to the bottom. Defaults to be 0 means no duration.
4 |
5 | ```vue
6 |
7 |
8 |
9 |
10 |
37 |
38 | ```
39 |
--------------------------------------------------------------------------------
/test/utils/index.test.js:
--------------------------------------------------------------------------------
1 | /**
2 | * 日志输出测试
3 | * 参考字符串来源:https://api.travis-ci.com/v3/job/196515104/log.txt
4 | */
5 |
6 | import {split2Lines} from '@/utils/index.js'
7 |
8 | const getStrCharCodes = str => {
9 | let codes = []
10 | for (let i = 0; i < str.length; i++) {
11 | codes.push(str.charCodeAt(i))
12 | }
13 | return codes
14 | }
15 |
16 | describe('src/utils/index.js', () => {
17 | it('根据换行符切割字符串', () => {
18 | const str =
19 | 'It should be split two lines.\n' + 'It should be split two lines.'
20 | const result = split2Lines(str)
21 |
22 | expect(result[0]).toBe('It should be split two lines.')
23 | expect(result[1]).toBe('It should be split two lines.')
24 | })
25 |
26 | it('当Backspace出现在换行符后不会切割字符串', () => {
27 | const str = 'It should be\n\u0008 only one line.'
28 | const result = split2Lines(str)
29 | let resultCharCode = getStrCharCodes(result[0])
30 | expect(result.length).toBe(1)
31 | expect([...resultCharCode]).toStrictEqual(getStrCharCodes(str))
32 | })
33 | })
34 |
--------------------------------------------------------------------------------
/src/components/loading.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | •
6 | •
7 | •
8 |
9 |
10 |
11 |
12 |
17 |
18 |
54 |
--------------------------------------------------------------------------------
/docs/auto-scroll.md:
--------------------------------------------------------------------------------
1 | auto-scroll 属性设置是否自动滚动到底部
2 |
3 | set auto-scroll true to auto scroll to bottom, and false to do nothing. Defaults to true.
4 |
5 | ```vue
6 |
7 |
8 |
9 |
36 |
41 |
42 | ```
43 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2019 FEMessage
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/notify.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # https://stackoverflow.com/questions/13872048/bash-script-what-does-bin-bash-mean
3 | echo "1/5: checking TRAVIS_TEST_RESULT"
4 | if [ "$TRAVIS_TEST_RESULT" != "0" ]
5 | then
6 | echo "build not success, bye"
7 | exit 1
8 | fi
9 |
10 | ORG_NAME=$(echo "$TRAVIS_REPO_SLUG" | cut -d '/' -f 1)
11 | REPO_NAME=$(echo "$TRAVIS_REPO_SLUG" | cut -d '/' -f 2)
12 |
13 | echo "2/5: pushing commit and tag to github"
14 | # 该命令很可能报错,但不影响实际进行,因而不能简单地在脚本开头 set -e
15 | git remote add github https://$GITHUB_TOKEN@github.com/$TRAVIS_REPO_SLUG.git > /dev/null 2>&1
16 | git push github HEAD:master --follow-tags
17 |
18 | echo "3/5: generating github release notes"
19 | GREN_GITHUB_TOKEN=$GITHUB_TOKEN yarn release
20 |
21 | # 避免发送错误信息
22 | if [ $? -ne 0 ]
23 | then
24 | echo "gren fails, bye"
25 | exit 1
26 | fi
27 |
28 | echo "4/5: downloading github release info"
29 | url=https://api.github.com/repos/$TRAVIS_REPO_SLUG/releases/latest
30 | resp_tmp_file=resp.tmp
31 |
32 | curl -H "Authorization: token $GITHUB_TOKEN" $url > $resp_tmp_file
33 |
34 | html_url=$(sed -n 5p $resp_tmp_file | sed 's/\"html_url\"://g' | awk -F '"' '{print $2}')
35 | body=$(grep body < $resp_tmp_file | sed 's/\"body\"://g;s/\"//g')
36 | version=$(echo $html_url | awk -F '/' '{print $NF}')
37 |
38 | echo "5/5: notifying with dingtalk bot"
39 | msg='{"msgtype": "markdown", "markdown": {"title": "'$REPO_NAME'更新", "text": "@所有人\n# ['$REPO_NAME'('$version')]('$html_url')\n'$body'"}}'
40 |
41 | curl -X POST https://oapi.dingtalk.com/robot/send\?access_token\=$DINGTALK_ROBOT_TOKEN -H 'Content-Type: application/json' -d "$msg"
42 |
43 | rm $resp_tmp_file
44 |
45 | echo "executing notify.sh successfully"
46 |
--------------------------------------------------------------------------------
/src/log-viewer.d.ts:
--------------------------------------------------------------------------------
1 | declare module '@femessage/log-viewer' {
2 | import Vue, {VueConstructor} from 'vue'
3 | class FemessageComponent extends Vue {
4 | static install(vue: typeof Vue): void
5 | }
6 |
7 | type CombinedVueInstance<
8 | Instance extends Vue,
9 | Data,
10 | Methods,
11 | Computed,
12 | Props
13 | > = Data & Methods & Computed & Props & Instance
14 |
15 | type ExtendedVue<
16 | Instance extends Vue,
17 | Data,
18 | Methods,
19 | Computed,
20 | Props
21 | > = VueConstructor<
22 | CombinedVueInstance & Vue
23 | >
24 |
25 | type Combined = Data &
26 | Methods &
27 | Computed &
28 | Props
29 |
30 | type LogViewerData = {
31 | start: number
32 | scrollStart: number
33 | animate: any
34 | LineWrapper: any
35 | }
36 |
37 | type LogViewerMethods = {}
38 |
39 | type LogViewerComputed = {}
40 |
41 | type LogViewerProps = {
42 | virtualAttrs: object
43 | rowHeight: number
44 | height: number
45 | log: string
46 | loading: boolean
47 | autoScroll: boolean
48 | hasNumber: boolean
49 | scrollDuration: number
50 | }
51 |
52 | type LogViewer = Combined<
53 | LogViewerData,
54 | LogViewerMethods,
55 | LogViewerComputed,
56 | LogViewerProps
57 | >
58 |
59 | export interface LogViewerType extends FemessageComponent, LogViewer {}
60 |
61 | const LogViewerConstruction: ExtendedVue<
62 | Vue,
63 | LogViewerData,
64 | LogViewerMethods,
65 | LogViewerComputed,
66 | LogViewerProps
67 | >
68 |
69 | export default LogViewerConstruction
70 | }
71 |
--------------------------------------------------------------------------------
/styleguide.config.js:
--------------------------------------------------------------------------------
1 | const {VueLoaderPlugin} = require('vue-loader')
2 | const path = require('path')
3 | const glob = require('glob')
4 |
5 | const sections = (() => {
6 | const docs = glob
7 | .sync('docs/*.md')
8 | .map(p => ({name: path.basename(p, '.md'), content: p}))
9 | const demos = []
10 | let faq = '' // 约定至多只有一个faq.md
11 | const guides = []
12 | docs.forEach(d => {
13 | if (/^faq$/i.test(d.name)) {
14 | d.name = d.name.toUpperCase()
15 | faq = d
16 | } else if (/^guide-/.test(d.name)) {
17 | guides.push(d)
18 | } else {
19 | demos.push(d)
20 | }
21 | })
22 | return [
23 | {
24 | name: 'Components',
25 | components: 'src/*.vue',
26 | usageMode: 'expand'
27 | },
28 | {
29 | name: 'Demo',
30 | sections: demos,
31 | sectionDepth: 2
32 | },
33 | ...(faq ? [faq] : []),
34 | ...(guides.length ? [{name: 'Guide', sections: guides}] : [])
35 | ]
36 | })()
37 |
38 | module.exports = {
39 | styleguideDir: 'docs',
40 | pagePerSection: true,
41 | ribbon: {
42 | url: 'https://github.com/FEMessage/log-viewer'
43 | },
44 | sections,
45 | webpackConfig: {
46 | module: {
47 | rules: [
48 | {
49 | test: /\.vue$/,
50 | loader: 'vue-loader'
51 | },
52 | {
53 | test: /\.js?$/,
54 | exclude: /node_modules/,
55 | loader: 'babel-loader'
56 | },
57 | {
58 | test: /\.css$/,
59 | loaders: ['style-loader', 'css-loader']
60 | },
61 | {
62 | test: /\.less$/,
63 | loaders: ['vue-style-loader', 'css-loader', 'less-loader']
64 | },
65 | {
66 | test: /\.(woff2?|eot|[ot]tf)(\?.*)?$/,
67 | loader: 'file-loader'
68 | }
69 | ]
70 | },
71 | plugins: [new VueLoaderPlugin()]
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/test/utils/ansi-parse.test.js:
--------------------------------------------------------------------------------
1 | import ansiParse from '@/utils/ansi-parse.js'
2 |
3 | describe('src/utils/ansi-parse.js', () => {
4 | it('解释日志字符串为可读数据', () => {
5 | const log =
6 | 'normal\x1b[30mcolor is black\x1b[0m\x1b[31;1mcolor is red and bold is true\x1b[0m\x1b[32;3mcolor is green and italic is true\x1b[0m\x1b[33;4mcolor is yellow and underline is true\x1b[0m\x1b[30;47mcolor is black and background is white\x1b[0m\x1b[30;39mcolor is reset\x1b[0m\x1b[40;49mbackground is reset\x1b[0m\x1b[1;22mbold is false\x1b[0m\x1b[3;23mitalic is false\x1b[0m\x1b[4;24munderline is false\x1b[0m'
7 | const result = ansiParse(log)
8 |
9 | expect(result[0]).toStrictEqual({
10 | text: 'normal'
11 | })
12 | expect(result[1]).toStrictEqual({
13 | foreground: 'black',
14 | text: 'color is black'
15 | })
16 | expect(result[2]).toStrictEqual({
17 | foreground: 'red',
18 | bold: true,
19 | text: 'color is red and bold is true'
20 | })
21 | expect(result[3]).toStrictEqual({
22 | foreground: 'green',
23 | italic: true,
24 | text: 'color is green and italic is true'
25 | })
26 | expect(result[4]).toStrictEqual({
27 | foreground: 'yellow',
28 | underline: true,
29 | text: 'color is yellow and underline is true'
30 | })
31 | expect(result[5]).toStrictEqual({
32 | foreground: 'black',
33 | background: 'white',
34 | text: 'color is black and background is white'
35 | })
36 | expect(result[6]).toStrictEqual({
37 | text: 'color is reset'
38 | })
39 | expect(result[7]).toStrictEqual({
40 | text: 'background is reset'
41 | })
42 | expect(result[8]).toStrictEqual({
43 | bold: false,
44 | text: 'bold is false'
45 | })
46 | expect(result[9]).toStrictEqual({
47 | italic: false,
48 | text: 'italic is false'
49 | })
50 | expect(result[10]).toStrictEqual({
51 | underline: false,
52 | text: 'underline is false'
53 | })
54 | })
55 | })
56 |
--------------------------------------------------------------------------------
/src/components/line-wrapper.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
63 |
90 |
--------------------------------------------------------------------------------
/src/components/line-content.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 | {{ item.text }}
17 |
18 |
19 |
30 |
31 |
98 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "@femessage/log-viewer",
3 | "version": "1.0.0",
4 | "description": "💻 View terminal logs in browser",
5 | "author": "https://github.com/FEMessage",
6 | "license": "MIT",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/FEMessage/log-viewer.git"
10 | },
11 | "keywords": [
12 | "vue",
13 | "sfc",
14 | "component"
15 | ],
16 | "files": [
17 | "src",
18 | "dist"
19 | ],
20 | "main": "dist/log-viewer.umd.js",
21 | "module": "dist/log-viewer.esm.js",
22 | "unpkg": "dist/log-viewer.min.js",
23 | "browser": {
24 | "./sfc": "src/log-viewer.vue"
25 | },
26 | "types": "src/log-viewer.d.ts",
27 | "scripts": {
28 | "dev": "vue-styleguidist server",
29 | "test": "jest --verbose",
30 | "doc": "vue-styleguidist build",
31 | "build": "npm run build:unpkg & npm run build:es & npm run build:umd & npm run doc",
32 | "build:umd": "rollup --config build/rollup.config.js --format umd --file dist/log-viewer.umd.js",
33 | "build:es": "rollup --config build/rollup.config.js --format es --file dist/log-viewer.esm.js",
34 | "build:unpkg": "rollup --config build/rollup.config.js --format iife --file dist/log-viewer.min.js",
35 | "precommit": "pretty-quick --staged",
36 | "stdver": "standard-version -m '[skip ci] chore(release): v%s'",
37 | "release": "gren release --override"
38 | },
39 | "dependencies": {
40 | "vue-virtual-scroll-list": "^1.4.1"
41 | },
42 | "devDependencies": {
43 | "@babel/core": "^7.4.3",
44 | "@babel/plugin-transform-runtime": "^7.4.3",
45 | "@babel/preset-env": "^7.4.3",
46 | "@femessage/github-release-notes": "latest",
47 | "babel-loader": "^8.0.5",
48 | "file-loader": "^3.0.1",
49 | "glob": "^7.1.3",
50 | "husky": "1.3.1",
51 | "jest": "^24.8.0",
52 | "less": "^3.9.0",
53 | "less-loader": "^5.0.0",
54 | "lint-staged": "^8.1.0",
55 | "minimist": "^1.2.0",
56 | "prettier": "1.18.2",
57 | "pretty-quick": "^1.4.1",
58 | "rollup": "^1.9.0",
59 | "rollup-plugin-babel": "^4.3.2",
60 | "rollup-plugin-commonjs": "^9.3.4",
61 | "rollup-plugin-terser": "^4.0.4",
62 | "rollup-plugin-vue": "^4.7.2",
63 | "standard-version": "^6.0.1",
64 | "stylelint": "^9.10.0",
65 | "stylelint-config-standard": "^18.2.0",
66 | "vue": "^2.6.10",
67 | "vue-loader": "^15.7.1",
68 | "vue-styleguidist": "^3.16.3",
69 | "vue-template-compiler": "^2.5.16",
70 | "webpack": "^4.29.6"
71 | },
72 | "publishConfig": {
73 | "access": "public"
74 | },
75 | "vue-sfc-cli": "1.12.0",
76 | "engines": {
77 | "node": ">= 4.0.0",
78 | "npm": ">= 3.0.0"
79 | },
80 | "jest": {
81 | "moduleNameMapper": {
82 | "^@/(.*)$": "/src/$1"
83 | }
84 | },
85 | "husky": {
86 | "hooks": {
87 | "pre-commit": "lint-staged",
88 | "post-commit": "git update-index --again"
89 | }
90 | },
91 | "lint-staged": {
92 | "*.(js|md|json)": [
93 | "prettier --write",
94 | "git add"
95 | ],
96 | "*.vue": [
97 | "prettier --write",
98 | "stylelint --fix",
99 | "git add"
100 | ]
101 | },
102 | "gren": "@femessage/grenrc"
103 | }
104 |
--------------------------------------------------------------------------------
/src/log-viewer.vue:
--------------------------------------------------------------------------------
1 |
2 |
15 |
16 |
17 |
190 |
198 |
--------------------------------------------------------------------------------
/src/utils/ansi-parse.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-plusplus, no-continue */
2 | // CSI解释器,参考
3 | // 字体颜色https://github.com/mozilla-frontend-infra/react-lazylog/blob/master/src/ansiparse.js
4 | const foregroundColors = {
5 | '30': 'black',
6 | '31': 'red',
7 | '32': 'green',
8 | '33': 'yellow',
9 | '34': 'blue',
10 | '35': 'magenta',
11 | '36': 'cyan',
12 | '37': 'white',
13 | '90': 'bright-black',
14 | '91': 'bright-red',
15 | '92': 'bright-green',
16 | '93': 'bright-yellow',
17 | '94': 'bright-blue',
18 | '95': 'bright-magenta',
19 | '96': 'bright-cyan',
20 | '97': 'bright-white'
21 | }
22 | // 字体背景
23 | const backgroundColors = {
24 | '40': 'black',
25 | '41': 'red',
26 | '42': 'green',
27 | '43': 'yellow',
28 | '44': 'blue',
29 | '45': 'magenta',
30 | '46': 'cyan',
31 | '47': 'white',
32 | '100': 'bright-black',
33 | '101': 'bright-red',
34 | '102': 'bright-green',
35 | '103': 'bright-yellow',
36 | '104': 'bright-blue',
37 | '105': 'bright-magenta',
38 | '106': 'bright-cyan',
39 | '107': 'bright-white'
40 | }
41 | // 文字状态(粗体,斜体,下划线)
42 | const styles = {
43 | '1': 'bold',
44 | '3': 'italic',
45 | '4': 'underline'
46 | }
47 | // Select Graphic Rendition(SGR) flag
48 | const SGRCode = 'm'
49 | // 非SGR,后续可针对该控制符进行解释
50 | const notSGRCodes = [
51 | 'A',
52 | 'B',
53 | 'C',
54 | 'D',
55 | 'E',
56 | 'F',
57 | 'G',
58 | 'H',
59 | 'J',
60 | 'K',
61 | 'S',
62 | 'T',
63 | 'f',
64 | 's',
65 | 'u',
66 | 'h',
67 | 'l'
68 | ]
69 |
70 | /**
71 | * 格式化数据类型
72 | * interface formatter {
73 | * text: string,
74 | * foreground?: string,
75 | * background?: string,
76 | * underline?: boolean,
77 | * bold?: boolean,
78 | * italic?: boolean,
79 | * }
80 | **/
81 |
82 | /**
83 | * Backspace操作,即删除最后一个字符
84 | * matchingText: string,
85 | * result: formatter[]
86 | * eraseChar: [string,formatter[]]
87 | * */
88 |
89 | const eraseChar = (matchingText, result) => {
90 | if (matchingText.length) {
91 | return [matchingText.substr(0, matchingText.length - 1), result]
92 | } else if (result.length) {
93 | const index = result.length - 1
94 | const {text} = result[index]
95 | const newResult =
96 | text.length === 1
97 | ? result.slice(0, result.length - 1)
98 | : result.map((item, i) =>
99 | index === i
100 | ? {...item, text: text.substr(0, text.length - 1)}
101 | : item
102 | )
103 |
104 | return [matchingText, newResult]
105 | }
106 |
107 | return [matchingText, result]
108 | }
109 |
110 | /**
111 | * str: string
112 | * ansiParse: formatter[]
113 | * This Function only translate these codes: 1,3,4,22-24,30-37,39,40-47,49,90-97,100-107.
114 | * It would be more and more powerful and translate more codes.
115 | **/
116 |
117 | const ansiParse = str => {
118 | let matchingControl = null
119 | let matchingCode = null
120 | let matchingText = ''
121 | let ansiState = []
122 | let result = []
123 | let state = {}
124 |
125 | for (let i = 0; i < str.length; i++) {
126 | if (matchingControl !== null) {
127 | if (matchingControl === '\x1b' && str[i] === '[') {
128 | if (matchingText) {
129 | state.text = matchingText
130 | result.push(state)
131 | state = {}
132 | matchingText = ''
133 | }
134 |
135 | matchingControl = null
136 | matchingCode = ''
137 | } else {
138 | matchingText += matchingControl + str[i]
139 | matchingControl = null
140 | }
141 |
142 | continue
143 | } else if (matchingCode !== null) {
144 | if (str[i] === ';') {
145 | ansiState.push(matchingCode)
146 | matchingCode = ''
147 | } else if (str[i] === SGRCode) {
148 | ansiState.push(matchingCode)
149 | matchingCode = null
150 | matchingText = ''
151 |
152 | for (let a = 0; a < ansiState.length; a++) {
153 | const ansiCode = +ansiState[a]
154 | if (foregroundColors[ansiCode]) {
155 | state.foreground = foregroundColors[ansiCode]
156 | } else if (backgroundColors[ansiCode]) {
157 | state.background = backgroundColors[ansiCode]
158 | } else if (ansiCode === 39) {
159 | delete state.foreground
160 | } else if (ansiCode === 49) {
161 | delete state.background
162 | } else if (styles[ansiCode]) {
163 | state[styles[ansiCode]] = true
164 | } else if (ansiCode === 22) {
165 | state.bold = false
166 | } else if (ansiCode === 23) {
167 | state.italic = false
168 | } else if (ansiCode === 24) {
169 | state.underline = false
170 | }
171 | }
172 |
173 | ansiState = []
174 | } else if (notSGRCodes.indexOf(str[i]) > -1) {
175 | // Ignore codes which is not SGR code and delete them.
176 | // It should be translated in some day.
177 | matchingCode = ''
178 | ansiState = []
179 | } else {
180 | matchingCode += str[i]
181 | }
182 |
183 | continue
184 | }
185 |
186 | if (str[i] === '\x1b') {
187 | // ESC Control
188 | matchingControl = str[i]
189 | } else if (str[i] === '\u0008') {
190 | // Backspace Control
191 | ;[matchingText, result] = eraseChar(matchingText, result)
192 | } else {
193 | matchingText += str[i]
194 | }
195 | }
196 |
197 | if (matchingText) {
198 | state.text = matchingText + (matchingControl || '')
199 | result.push(state)
200 | }
201 |
202 | return result
203 | }
204 |
205 | export default ansiParse
206 |
--------------------------------------------------------------------------------
/README-zh.md:
--------------------------------------------------------------------------------
1 | # log-viewer
2 |
3 | [](https://travis-ci.com/FEMessage/log-viewer)
4 | [](https://www.npmjs.com/package/@femessage/log-viewer)
5 | [](https://www.npmjs.com/package/@femessage/log-viewer)
6 | [](https://github.com/FEMessage/log-viewer/blob/master/LICENSE)
7 | [](https://github.com/FEMessage/log-viewer/pulls)
8 | [](https://github-tools.github.io/github-release-notes/)
9 |
10 | 日志查看组件,将终端日志内容展示在页面中
11 |
12 | 
13 |
14 | [English](./README-en.md)
15 |
16 | ## Table of Contents
17 |
18 | - [Features](#features)
19 | - [Install](#install)
20 | - [Usage](#usage)
21 | - [Links](#links)
22 | - [Reference](#reference)
23 | - [Performance](#performance)
24 | - [内存占用](#内存占用)
25 | - [item-mode](#item-mode)
26 | - [vfor-mode](#vfor-mode)
27 | - [render 耗时](#render-耗时)
28 | - [item-mode](#item-mode-1)
29 | - [vfor-mode](#vfor-mode-1)
30 | - [Contributing](#contributing)
31 | - [Contributors](#contributors)
32 | - [License](#license)
33 |
34 | ## Features
35 |
36 | - 对日志流中特殊字符进行了处理
37 | - 高性能,处理大量数据不卡顿
38 | - 可自定义 loading 样式
39 | - 可自动滚动到底部
40 |
41 | [⬆ Back to Top](#table-of-contents)
42 |
43 | ## Install
44 |
45 | ```bash
46 | yarn add @femessage/log-viewer
47 | ```
48 |
49 | [⬆ Back to Top](#table-of-contents)
50 |
51 | ## Usage
52 |
53 | ```html
54 |
55 | ```
56 | [⬆ Back to Top](#table-of-contents)
57 |
58 | ## Links
59 |
60 | - [设计文档](https://www.yuque.com/docs/share/db3640ad-ab65-4588-8244-d245f90e9a6a#)
61 |
62 | [⬆ Back to Top](#table-of-contents)
63 |
64 | ## Reference
65 |
66 | - [thanks to react-lazylog](https://github.com/mozilla-frontend-infra/react-lazylog)
67 | - [travis-ci logs](https://travis-ci.org/)
68 | - [http://jafrog.com/2013/11/23/colors-in-terminal.html](http://jafrog.com/2013/11/23/colors-in-terminal.html)
69 | - [https://en.wikipedia.org/wiki/ANSI_escape_code](https://en.wikipedia.org/wiki/ANSI_escape_code)
70 |
71 | [⬆ Back to Top](#table-of-contents)
72 |
73 | ## Performance
74 |
75 | 虚拟滚动使用的是:[https://github.com/tangbc/vue-virtual-scroll-list](https://github.com/tangbc/vue-virtual-scroll-list)
76 |
77 | 内部实现使用的 item-mode,因为比 vfor-mode 性能更好。
78 |
79 | ### 内存占用
80 |
81 | 使用 100,000 条数据进行测试
82 |
83 | #### item-mode
84 |
85 | 组件渲染前页面内存:36.5MB
组件渲染后内存:48MB
内存消耗:11.5MB

86 |
87 | #### vfor-mode
88 |
89 | 组件渲染前页面内存:43MB
90 | 组件渲染后内存:221MB
91 | 内存消耗:178MB
92 |
93 | 
94 |
95 | ### render 耗时
96 |
97 | 使用 100,000 条数据进行测试
98 |
99 | #### item-mode
100 |
101 | render 时间:0.63ms
102 | patch 时间: 72.18ms
103 | 总时间:72.85ms
104 | 
105 |
106 | #### vfor-mode
107 |
108 | render 时间:933.05ms
109 | patch 时间: 23.81ms
110 | 总时间:956.86ms
111 | 
112 |
113 | [⬆ Back to Top](#table-of-contents)
114 |
115 | ## Contributing
116 |
117 | For those who are interested in contributing to this project, such as:
118 |
119 | - report a bug
120 | - request new feature
121 | - fix a bug
122 | - implement a new feature
123 |
124 | Please refer to our [contributing guide](https://github.com/FEMessage/.github/blob/master/CONTRIBUTING.md).
125 |
126 | [⬆ Back to Top](#table-of-contents)
127 |
128 | ## Contributors
129 |
130 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
140 |
141 | [⬆ Back to Top](#table-of-contents)
142 |
143 | ## License
144 |
145 | [MIT](./LICENSE)
146 |
147 | [⬆ Back to Top](#table-of-contents)
148 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # log-viewer
2 |
3 | [](https://travis-ci.com/FEMessage/log-viewer)
4 | [](https://www.npmjs.com/package/@femessage/log-viewer)
5 | [](https://www.npmjs.com/package/@femessage/log-viewer)
6 | [](https://github.com/FEMessage/log-viewer/blob/master/LICENSE)
7 | [](https://github.com/FEMessage/log-viewer/pulls)
8 | [](https://github-tools.github.io/github-release-notes/)
9 |
10 | log-viewer is a vue component which can display terminal log in browser with high performance.
11 |
12 | 
13 |
14 | [中文文档](./README-zh.md)
15 |
16 | ## Table of Contents
17 |
18 | - [Features](#features)
19 | - [Install](#install)
20 | - [Usage](#usage)
21 | - [Links](#links)
22 | - [Reference](#reference)
23 | - [Performance](#performance)
24 | - [Memory Usage](#memory-usage)
25 | - [item-mode](#item-mode)
26 | - [vfor-mode](#vfor-mode)
27 | - [Render timeline](#render-timeline)
28 | - [item-mode](#item-mode-1)
29 | - [vfor-mode](#vfor-mode-1)
30 | - [Contributing](#contributing)
31 | - [Contributors](#contributors)
32 | - [License](#license)
33 |
34 | ## Features
35 |
36 | - Process some special characters in the log stream
37 | - High performance, and process large amounts of data without jamming
38 | - Customize loading status
39 | - Auto scroll to the bottom
40 |
41 | [⬆ Back to Top](#table-of-contents)
42 |
43 | ## Install
44 |
45 | ```bash
46 | yarn add @femessage/log-viewer
47 | ```
48 |
49 | [⬆ Back to Top](#table-of-contents)
50 |
51 | ## Usage
52 |
53 | ```html
54 |
55 | ```
56 |
57 | [⬆ Back to Top](#table-of-contents)
58 |
59 | ## Links
60 |
61 | - [design doc](https://www.yuque.com/docs/share/db3640ad-ab65-4588-8244-d245f90e9a6a?translate=en)
62 |
63 | [⬆ Back to Top](#table-of-contents)
64 |
65 | ## Reference
66 |
67 | - [thanks to react-lazylog](https://github.com/mozilla-frontend-infra/react-lazylog)
68 | - [travis-ci logs](https://travis-ci.org/)
69 | - [http://jafrog.com/2013/11/23/colors-in-terminal.html](http://jafrog.com/2013/11/23/colors-in-terminal.html)
70 | - [https://en.wikipedia.org/wiki/ANSI_escape_code](https://en.wikipedia.org/wiki/ANSI_escape_code)
71 |
72 | [⬆ Back to Top](#table-of-contents)
73 |
74 | ## Performance
75 |
76 | Virtual scrolling use the component:[https://github.com/tangbc/vue-virtual-scroll-list](https://github.com/tangbc/vue-virtual-scroll-list)
77 |
78 | Achieve internal use 'item-mode' so that its performance is better than the 'vfor-mode'.
79 |
80 | ### Memory Usage
81 |
82 | Use 100,000 lines to test.
83 |
84 | #### item-mode
85 |
86 | The Memory before the component mount: 36.5MB
87 | The Memory after the component mounted: 48MB
88 | Memory Usage: 11.5MB
89 | 
90 |
91 | #### vfor-mode
92 |
93 | The Memory before the component mount: 43MB
94 | The Memory after the component mounted: 221MB
95 | Memory Usage: 178MB
96 |
97 | 
98 |
99 | ### Render timeline
100 |
101 | Also use 100,000 lines to test.
102 |
103 | #### item-mode
104 |
105 | Render time: 0.63ms
106 | Patch time: 72.18ms
107 | Total time: 72.85ms
108 | 
109 |
110 | #### vfor-mode
111 |
112 | Render time: 933.05ms
113 | Patch time: 23.81ms
114 | Total time: 956.86ms
115 | 
116 |
117 | [⬆ Back to Top](#table-of-contents)
118 |
119 | ## Contributing
120 |
121 | For those who are interested in contributing to this project, such as:
122 |
123 | - report a bug
124 | - request new feature
125 | - fix a bug
126 | - implement a new feature
127 |
128 | Please refer to our [contributing guide](https://github.com/FEMessage/.github/blob/master/CONTRIBUTING.md).
129 |
130 | [⬆ Back to Top](#table-of-contents)
131 |
132 | ## Contributors
133 |
134 | Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)):
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 | This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. Contributions of any kind welcome!
143 |
144 | [⬆ Back to Top](#table-of-contents)
145 |
146 | ## License
147 |
148 | [MIT](./LICENSE)
149 |
150 | [⬆ Back to Top](#table-of-contents)
151 |
--------------------------------------------------------------------------------