├── .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 | 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 | 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 | 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 | 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 | 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 | 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 | 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 | 9 | 63 | 90 | -------------------------------------------------------------------------------- /src/components/line-content.vue: -------------------------------------------------------------------------------- 1 | 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 | 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 | [![Build Status](https://badgen.net/travis/FEMessage/log-viewer/master)](https://travis-ci.com/FEMessage/log-viewer) 4 | [![NPM Download](https://badgen.net/npm/dm/@femessage/log-viewer)](https://www.npmjs.com/package/@femessage/log-viewer) 5 | [![NPM Version](https://badgen.net/npm/v/@femessage/log-viewer)](https://www.npmjs.com/package/@femessage/log-viewer) 6 | [![NPM License](https://badgen.net/npm/license/@femessage/log-viewer)](https://github.com/FEMessage/log-viewer/blob/master/LICENSE) 7 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/FEMessage/log-viewer/pulls) 8 | [![Automated Release Notes by gren](https://img.shields.io/badge/%F0%9F%A4%96-release%20notes-00B2EE.svg)](https://github-tools.github.io/github-release-notes/) 9 | 10 | 日志查看组件,将终端日志内容展示在页面中 11 | 12 | ![new-log](https://user-images.githubusercontent.com/53422750/65296811-4cb5b700-db98-11e9-9b55-1a5c8633ae8f.gif) 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
![图片.gif](https://cdn.nlark.com/yuque/0/2019/gif/298847/1563439757710-ffb3b170-839c-46ca-810e-06e041fe93bd.gif#align=left&display=inline&height=425&name=%E5%9B%BE%E7%89%87.gif&originHeight=425&originWidth=720&size=266064&status=done&width=720) 86 | 87 | #### vfor-mode 88 | 89 | 组件渲染前页面内存:43MB
90 | 组件渲染后内存:221MB
91 | 内存消耗:178MB 92 | 93 | ![item-10e4.gif](https://cdn.nlark.com/yuque/0/2019/gif/298847/1563439644472-91058ae0-804e-4d03-bd9c-3cb116283a3c.gif#align=left&display=inline&height=425&name=item-10e4.gif&originHeight=425&originWidth=720&size=270167&status=done&width=720) 94 | 95 | ### render 耗时 96 | 97 | 使用 100,000 条数据进行测试 98 | 99 | #### item-mode 100 | 101 | render 时间:0.63ms
102 | patch 时间: 72.18ms
103 | 总时间:72.85ms
104 | ![item-1e5.gif](https://cdn.nlark.com/yuque/0/2019/gif/298847/1563849440263-9ee2e04b-eac3-49fc-8ec5-b4ac63f77f8b.gif#align=left&display=inline&height=540&name=item-1e5.gif&originHeight=540&originWidth=1081&size=175825&status=done&width=1081) 105 | 106 | #### vfor-mode 107 | 108 | render 时间:933.05ms
109 | patch 时间: 23.81ms
110 | 总时间:956.86ms
111 | ![v-for-1e5.gif](https://cdn.nlark.com/yuque/0/2019/gif/298847/1563849485762-ea08a525-f04c-4827-ab1d-5242d92b80ba.gif#align=left&display=inline&height=811&name=v-for-1e5.gif&originHeight=811&originWidth=1211&size=94294&status=done&width=1211) 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 |
snowlocked
snowlocked

💻 📖 ⚠️
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 | [![Build Status](https://badgen.net/travis/FEMessage/log-viewer/master)](https://travis-ci.com/FEMessage/log-viewer) 4 | [![NPM Download](https://badgen.net/npm/dm/@femessage/log-viewer)](https://www.npmjs.com/package/@femessage/log-viewer) 5 | [![NPM Version](https://badgen.net/npm/v/@femessage/log-viewer)](https://www.npmjs.com/package/@femessage/log-viewer) 6 | [![NPM License](https://badgen.net/npm/license/@femessage/log-viewer)](https://github.com/FEMessage/log-viewer/blob/master/LICENSE) 7 | [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/FEMessage/log-viewer/pulls) 8 | [![Automated Release Notes by gren](https://img.shields.io/badge/%F0%9F%A4%96-release%20notes-00B2EE.svg)](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 | ![new-log](https://user-images.githubusercontent.com/53422750/65296811-4cb5b700-db98-11e9-9b55-1a5c8633ae8f.gif) 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 | ![图片.gif](https://cdn.nlark.com/yuque/0/2019/gif/298847/1563439757710-ffb3b170-839c-46ca-810e-06e041fe93bd.gif#align=left&display=inline&height=425&name=%E5%9B%BE%E7%89%87.gif&originHeight=425&originWidth=720&size=266064&status=done&width=720) 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 | ![item-10e4.gif](https://cdn.nlark.com/yuque/0/2019/gif/298847/1563439644472-91058ae0-804e-4d03-bd9c-3cb116283a3c.gif#align=left&display=inline&height=425&name=item-10e4.gif&originHeight=425&originWidth=720&size=270167&status=done&width=720) 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 | ![item-1e5.gif](https://cdn.nlark.com/yuque/0/2019/gif/298847/1563849440263-9ee2e04b-eac3-49fc-8ec5-b4ac63f77f8b.gif#align=left&display=inline&height=540&name=item-1e5.gif&originHeight=540&originWidth=1081&size=175825&status=done&width=1081) 109 | 110 | #### vfor-mode 111 | 112 | Render time: 933.05ms
113 | Patch time: 23.81ms
114 | Total time: 956.86ms
115 | ![v-for-1e5.gif](https://cdn.nlark.com/yuque/0/2019/gif/298847/1563849485762-ea08a525-f04c-4827-ab1d-5242d92b80ba.gif#align=left&display=inline&height=811&name=v-for-1e5.gif&originHeight=811&originWidth=1211&size=94294&status=done&width=1211) 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 |
snowlocked
snowlocked

💻 📖 ⚠️
EVILLT
EVILLT

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