├── public ├── CNAME ├── favicon.ico ├── database-table-icon-10.jpg ├── index.html └── privacy_policy_chmreaderx.html ├── .vscode ├── settings.json ├── tasks.json └── launch.json ├── .postcssrc.js ├── src ├── assets │ └── logo.png ├── tdSample.vue ├── App.vue ├── Untitled.code-workspace ├── shims-vue.d.ts ├── models │ ├── Lazy.ts │ ├── TableParam.ts │ ├── TreeUtil.spec.ts │ ├── History.spec.ts │ ├── History.ts │ ├── TreeState.spec.ts │ ├── TreeUtil.ts │ ├── TDVOption.ts │ ├── TableState.ts │ ├── TableUtil.ts │ └── TreeState.ts ├── util │ ├── __snapshots__ │ │ └── Util.spec.ts.snap │ ├── Util.spec.ts │ ├── TextLine.ts │ └── Util.ts ├── parsers │ ├── __snapshots__ │ │ ├── CSVParserPlugin.spec.ts.snap │ │ ├── JSONParserPlugin.spec.ts.snap │ │ ├── PrometheusParser.spec.ts.snap │ │ └── YAMLParserPlugin.spec.ts.snap │ ├── CSVParserPlugin.spec.ts │ ├── XMLParserPlugin.spec.ts │ ├── JSONParserPlugin.spec.ts │ ├── PrometheusParser.spec.ts │ ├── YAMLParserPlugin.spec.ts │ ├── PrometheusParserPlugin.ts │ ├── CSVParserPlugin.ts │ ├── JSONParserPlugin.ts │ ├── YAMLParserPlugin.ts │ ├── XMLParserPlugin.ts │ └── PrometheusParser.ts ├── shims-tsx.d.ts ├── components │ ├── td-Key1.vue │ ├── DataFilter.ts │ ├── JsonPath.vue │ ├── TreeView.vue │ ├── SimpleValue.vue │ ├── td-Value.vue │ ├── ExpandControl.vue │ ├── Vue2DataTable.ts │ ├── SourceView.vue │ ├── th-Filter.vue │ ├── TreeViewItem.vue │ ├── JsonTreeTable.vue │ └── JsonTable.vue ├── UrlParam.ts ├── main.ts ├── lib.ts ├── TableTest.vue ├── Home.vue └── sampleData.ts ├── vite.config.js ├── .gitignore ├── .github └── workflows │ └── ci.yml ├── tsconfig.json ├── vue.config.js ├── .eslintrc.js ├── tslint.json ├── LICENSE.txt ├── sample └── embedded.html ├── package.json └── README.md /public/CNAME: -------------------------------------------------------------------------------- 1 | www.treedoc.org -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "jshint.options": {"esversion": 6} 3 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treedoc/TreedocViewer/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treedoc/TreedocViewer/HEAD/src/assets/logo.png -------------------------------------------------------------------------------- /public/database-table-icon-10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/treedoc/TreedocViewer/HEAD/public/database-table-icon-10.jpg -------------------------------------------------------------------------------- /src/tdSample.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/Untitled.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": ".." 5 | }, 6 | { 7 | "path": "../../treedoc_ts" 8 | }, 9 | { 10 | "path": "../../download" 11 | } 12 | ], 13 | "settings": {} 14 | } -------------------------------------------------------------------------------- /src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue'; 3 | export default Vue; 4 | } 5 | 6 | declare module 'msplit'; 7 | declare module 'vue2-datatable-component'; 8 | declare module 'vue-codemirror-lite'; 9 | -------------------------------------------------------------------------------- /src/models/Lazy.ts: -------------------------------------------------------------------------------- 1 | export default class Lazy { 2 | private val: T|null = null; 3 | get(supplier: () => T) { 4 | if (this.val !== null) { 5 | this.val = supplier(); 6 | } 7 | return this.val!; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/util/__snapshots__/Util.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1 2 | 3 | exports[`Util.ts > topLines 1`] = ` 4 | { 5 | "length": 9, 6 | "numLines": 3, 7 | } 8 | `; 9 | 10 | exports[`Util.ts > topLines 2`] = ` 11 | { 12 | "length": 6, 13 | "numLines": 2, 14 | } 15 | `; 16 | -------------------------------------------------------------------------------- /src/parsers/__snapshots__/CSVParserPlugin.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1 2 | 3 | exports[`CSVParser.ts > parse 1`] = `"[{field1: 'v11', field2: 'v12', field3: 'v13'}, {field1: 'v21', field2: 'v2l1\\\\nV2l2', field3: 'v23'}, {field1: 'v31\\"v31', field2: 'v32\\"\\"v32', field3: 'v33'}]"`; 4 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import {defineConfig} from 'vite'; 2 | 3 | export default defineConfig({ 4 | test: { 5 | globals: true, 6 | // setupFiles: 'src/setupTests.js', 7 | }, 8 | resolve: { 9 | alias: [ 10 | { find: '@', replacement: 'src' }, 11 | ], 12 | }, 13 | }); -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | 4 | # local env files 5 | .env.local 6 | .env.*.local 7 | 8 | # Log files 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | 13 | # Editor directories and files 14 | .idea 15 | # .vscode 16 | *.suo 17 | *.ntvs* 18 | *.njsproj 19 | *.sln 20 | *.sw* 21 | dist 22 | /coverage 23 | -------------------------------------------------------------------------------- /src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue'; 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode {} 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue {} 9 | interface IntrinsicElements { 10 | [elem: string]: any; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "type": "npm", 8 | "script": "serve", 9 | "problemMatcher": [] 10 | }, 11 | { 12 | "type": "npm", 13 | "script": "test:unit", 14 | "problemMatcher": [] 15 | } 16 | ] 17 | } -------------------------------------------------------------------------------- /src/models/TableParam.ts: -------------------------------------------------------------------------------- 1 | import { TableConfig, DataTableOptions } from '@/components/Vue2DataTable'; 2 | 3 | /** 4 | * TableParam is a JSON object that passed through URL or event to provide all the information to construct 5 | * an JsonTable component 6 | */ 7 | export default interface TableParam { 8 | title?: string; 9 | jsonData: any | string; 10 | initialPath?: string | null; 11 | tableConfig?: TableConfig; 12 | } 13 | -------------------------------------------------------------------------------- /src/parsers/CSVParserPlugin.spec.ts: -------------------------------------------------------------------------------- 1 | import sampleData from '../sampleData'; 2 | import CSVParserPlugin from './CSVParserPlugin'; 3 | import { describe, expect, test } from 'vitest' 4 | 5 | describe('CSVParser.ts', () => { 6 | const parser = new CSVParserPlugin(); 7 | test('looksLike', () => { 8 | expect(parser.looksLike(sampleData.csvStr)).toBeTruthy(); 9 | expect(parser.looksLike(sampleData.yamlStr)).toBeFalsy(); 10 | }); 11 | 12 | test('parse', () => { 13 | const result = parser.parse(sampleData.csvStr); 14 | expect(result.message).toBe('CSVParser.parse()'); 15 | expect(result.result?.toStringInternal("", true, true)).toMatchSnapshot(); 16 | }); 17 | }); 18 | -------------------------------------------------------------------------------- /src/parsers/__snapshots__/JSONParserPlugin.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1 2 | 3 | exports[`JSONParser.ts > parse 1`] = `"{refundAmtMoney: 'USD 15.32', activityHistory: [{$type: 'ActivityHist', $id: 1234, creationDate: '2014/10/02 10:20:37', lastModifiedDate: '2014/10/02 10:20:37', timeStamp: 1599461650448, runtimeContext: 't=118', partitionKey: 0, activityType: '1-buyerCreateCancel', log: 'http://www.google.com'}, {$type: 'ActivityHistBoImpl', creationDate: '2014/10/02 11:15:13', lastModifiedDate: '2014/10/02 11:15:13', timeStamp: 1599481650448, runtimeContext: 'm=t=148\\\\nline2', partitionKey: 0, activityType: '6-sellerApprove'}], current: {$ref: '#/activityHistory/1'}, first: {$ref: '#1234'}}"`; 4 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 3 | "version": "0.2.0", 4 | "configurations": [ 5 | { 6 | "runtimeExecutable": "/Users/jianwu.chen/.nvm/versions/node/v16.17.0/bin/node", 7 | "type": "pwa-node", 8 | "request": "launch", 9 | "name": "Debug Current Test File", 10 | "autoAttachChildProcesses": true, 11 | "skipFiles": ["/**", "**/node_modules/**"], 12 | "program": "${workspaceRoot}/node_modules/vitest/vitest.mjs", 13 | "args": ["run", "${relativeFile}"], 14 | "smartStep": true, 15 | "console": "integratedTerminal" 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /src/models/TreeUtil.spec.ts: -------------------------------------------------------------------------------- 1 | import TreeUtil from './TreeUtil'; 2 | import { TD, TDJSONParser, TDJSONParserOption } from 'treedoc/lib/index'; 3 | import { describe, expect, test } from 'vitest' 4 | 5 | describe('TreeUtil.ts', () => { 6 | test('getTypeSizeLabel', () => { 7 | const json = ` 8 | { 9 | $type:'ActivityHist', 10 | $id:1234, 11 | creationDate:'2014/10/02 10:20:37', 12 | lastModifiedDate:'2014/10/02 10:20:37', 13 | runtimeContext:'t=118', 14 | partitionKey:0, 15 | activityType:'1-buyerCreateCancel', 16 | log:'http://www.google.com', 17 | } 18 | `; 19 | const tdNode = TDJSONParser.get().parse(json); 20 | 21 | expect(TreeUtil.getTypeSizeLabel(tdNode)).toBe('{8} '); 22 | }); 23 | }); -------------------------------------------------------------------------------- /src/util/Util.spec.ts: -------------------------------------------------------------------------------- 1 | import Util from './Util'; 2 | import { describe, expect, test } from 'vitest' 3 | 4 | describe('Util.ts', () => { 5 | test('nonBlankStartsWith', () => { 6 | expect(Util.nonBlankStartsWith(' abcdefghijklmn', ['ab', 'bc]'], 5)).toBeTruthy(); 7 | expect(Util.nonBlankStartsWith(' bcdefghijklmn', ['ab', 'bc]'], 5)).toBeFalsy(); 8 | }); 9 | 10 | test('nonBlankEndsWith', () => { 11 | expect(Util.nonBlankEndsWith(' abcdefghijklmn \t', ['mn', 'n'], 5)).toBeTruthy(); 12 | expect(Util.nonBlankEndsWith(' bcdefghijklm \n\t ', ['mn', 'n'], 5)).toBeFalsy(); 13 | }); 14 | 15 | test('topLines', () => { 16 | expect(Util.topLines('12\n34\n56\n', 10)).toMatchSnapshot(); 17 | expect(Util.topLines('12\n34\n56\n', 7)).toMatchSnapshot(); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /src/util/TextLine.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { Bookmark } from 'treedoc'; 3 | 4 | export default class TextLine { 5 | lineOffsets: number[] = []; 6 | constructor(private text: string) { 7 | let newLine = true; 8 | for (let i = 0; i < text.length; i++) { 9 | if (newLine) 10 | this.lineOffsets.push(i); 11 | newLine = false; 12 | if (text.charAt(i) === '\n') 13 | newLine = true; 14 | } 15 | } 16 | 17 | getBookmark(offset: number) { 18 | let line = _.sortedIndex(this.lineOffsets, offset) 19 | let column = 0; 20 | if (offset === this.lineOffsets.length || offset < this.lineOffsets[line]) { 21 | line --; 22 | column = offset - this.lineOffsets[line]; 23 | } 24 | return new Bookmark(line, column, offset); 25 | } 26 | } -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | TreedocViewer 9 | 10 | 11 | 17 |
18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | # reference: https://help.github.com/en/articles/configuring-a-workflow 2 | name: Node CI 3 | 4 | on: 5 | push: 6 | branches: 7 | - master 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [16.x] 17 | 18 | steps: 19 | - uses: actions/checkout@v1 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v1 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - name: npm install, build, and test 25 | run: | 26 | npm ci 27 | npm run build 28 | # npm test 29 | env: 30 | CI: true 31 | # - name: codecov 32 | # run: npx codecov 33 | # env: 34 | # CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} 35 | -------------------------------------------------------------------------------- /src/models/History.spec.ts: -------------------------------------------------------------------------------- 1 | import History from './History'; 2 | import { describe, expect, test } from 'vitest' 3 | 4 | describe('History.ts', () => { 5 | test('History should works', () => { 6 | const hist = new History(); 7 | expect(hist.canBack()).toBe(false); 8 | expect(hist.canForward()).toBe(false); 9 | 10 | hist.append('first'); 11 | expect(hist.canBack()).toBe(false); 12 | expect(hist.canForward()).toBe(false); 13 | 14 | hist.append('second'); 15 | expect(hist.canBack()).toBe(true); 16 | expect(hist.canForward()).toBe(false); 17 | 18 | expect(hist.back()).toBe('first'); 19 | expect(hist.canForward()).toBe(true); 20 | expect(hist.canBack()).toBe(false); 21 | 22 | expect(hist.forward()).toBe('second'); 23 | expect(hist.canForward()).toBe(false); 24 | expect(hist.canBack()).toBe(true); 25 | }); 26 | }) 27 | -------------------------------------------------------------------------------- /src/components/td-Key1.vue: -------------------------------------------------------------------------------- 1 | 9 | 26 | -------------------------------------------------------------------------------- /src/models/History.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * History maintains a linear history path, the current position can be moved backward or forward 3 | * in the history stack. It won't store branching information, once append a new event at a particular position 4 | * the history stack after the pos will be trimmed. So that only a linear path will be maintained. 5 | */ 6 | export default class History { 7 | items: T[] = []; 8 | pos = -1; 9 | 10 | canBack() { return this.pos > 0; } 11 | back() { 12 | if (!this.canBack()) 13 | return this.items[this.pos]; 14 | return this.items[--this.pos]; 15 | } 16 | 17 | canForward() { return this.pos < this.items.length - 1; } 18 | forward() { 19 | if (!this.canForward()) 20 | return this.items[this.pos]; 21 | return this.items[++this.pos]; 22 | } 23 | 24 | append(element: T) { 25 | this.items.length = ++this.pos; 26 | this.items.push(element); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/parsers/XMLParserPlugin.spec.ts: -------------------------------------------------------------------------------- 1 | import sampleData from '../sampleData'; 2 | import XMLParserPlugin from '../parsers/XMLParserPlugin'; 3 | import { describe, expect, test } from 'vitest' 4 | 5 | /** 6 | * @vitest-environment jsdom 7 | */ 8 | describe('JSONParser.ts', () => { 9 | const parser = new XMLParserPlugin(); 10 | test('looksLike', () => { 11 | expect(parser.looksLike(sampleData.xmlStr)).toBeTruthy(); 12 | expect(parser.looksLike(sampleData.jsonStr)).toBeFalsy(); 13 | }); 14 | 15 | test('parse', () => { 16 | const result = parser.parse(sampleData.xmlStr); 17 | expect(result.message).toBe('DOMParser().parseFromString()'); 18 | }); 19 | 20 | test('parse Compacted', () => { 21 | const compactParser = new XMLParserPlugin('XML compact', 'text/xml', true); 22 | const result = compactParser.parse(sampleData.xmlStr); 23 | expect(result.message).toBe('DOMParser().parseFromString()'); 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "esnext", 5 | "strict": true, 6 | "declaration": true, 7 | "declarationDir": "dist/lib", 8 | "jsx": "preserve", 9 | "importHelpers": true, 10 | "moduleResolution": "node", 11 | "experimentalDecorators": true, 12 | "esModuleInterop": true, 13 | "allowSyntheticDefaultImports": true, 14 | "sourceMap": true, 15 | "baseUrl": ".", 16 | "types": [ 17 | "webpack-env", 18 | "node" 19 | ], 20 | "paths": { 21 | "@/*": [ 22 | "src/*" 23 | ] 24 | }, 25 | "lib": [ 26 | "esnext", 27 | "dom", 28 | "dom.iterable", 29 | "scripthost" 30 | ] 31 | }, 32 | "include": [ 33 | "src/**/*.ts", 34 | "src/**/*.tsx", 35 | "src/**/*.vue", 36 | ], 37 | "exclude": [ 38 | "tests/**/*.tsx", 39 | "tests/**/*.ts", 40 | "**/*.spec.ts", 41 | "node_modules" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /src/parsers/JSONParserPlugin.spec.ts: -------------------------------------------------------------------------------- 1 | import sampleData from '../sampleData'; 2 | import JSONParserPlugin, { JSONParserType } from './JSONParserPlugin'; 3 | import { describe, expect, test } from 'vitest' 4 | 5 | describe('JSONParser.ts', () => { 6 | const parser = new JSONParserPlugin(); 7 | const parserMapToString = new JSONParserPlugin('Map.toString', JSONParserType.JAVA_MAP_TO_STRING); 8 | test('looksLike', () => { 9 | expect(parser.looksLike(sampleData.jsonStr)).toBeTruthy(); 10 | expect(parser.looksLike(sampleData.yamlStr)).toBeFalsy(); 11 | 12 | expect(parserMapToString.looksLike(sampleData.mapToStringStr)).toBeTruthy(); 13 | expect(parserMapToString.looksLike(sampleData.xmlStr)).toBeFalsy(); 14 | }); 15 | 16 | test('parse', () => { 17 | const result = parser.parse(sampleData.jsonStr); 18 | expect(result.message).toBe('TDJSONParser.parse()'); 19 | expect(result.result?.toStringInternal("", true, true)).toMatchSnapshot(); 20 | }); 21 | }); -------------------------------------------------------------------------------- /vue.config.js: -------------------------------------------------------------------------------- 1 | /** 2 | * These are some necessary steps changing the default webpack config of the Vue CLI 3 | * that need to be changed in order for TypeScript based components to generate their 4 | * declaration (.d.ts) files. 5 | * Code by various users from https://github.com/vuejs/vue-cli/issues/1081 6 | */ 7 | const fixEmitDeclarationFilesForTypeScript = { 8 | chainWebpack: (config) => { 9 | if (process.env.NODE_ENV === 'production') { 10 | config.module.rule('ts').uses.delete('cache-loader'); 11 | config.module 12 | .rule('ts') 13 | .use('ts-loader') 14 | .loader('ts-loader') 15 | .tap((options) => ({ 16 | ...options, 17 | transpileOnly: false, 18 | happyPackMode: false, 19 | })); 20 | } 21 | }, 22 | parallel: false, 23 | }; 24 | 25 | module.exports = { 26 | ...fixEmitDeclarationFilesForTypeScript, 27 | publicPath: './', 28 | css: { 29 | extract: false, 30 | }, 31 | }; -------------------------------------------------------------------------------- /src/parsers/PrometheusParser.spec.ts: -------------------------------------------------------------------------------- 1 | import {prometheusStr} from '../sampleData'; 2 | 3 | import { describe, expect, test } from 'vitest' 4 | import PrometheusParser from './PrometheusParser'; 5 | import { TD } from 'treedoc'; 6 | 7 | // TODO: test following data 8 | const data = ` 9 | # HELP thanos_objstore_bucket_operation_duration_seconds Duration of successful operations against the bucket 10 | # TYPE thanos_objstore_bucket_operation_duration_seconds histogram 11 | thanos_objstore_bucket_operation_duration_seconds_bucket{bucket="thanos",operation="attributes",le="0.01"} 0 12 | thanos_objstore_bucket_operation_duration_seconds_bucket{bucket="thanos",operation="attributes",le="0.1"} 0 13 | ` 14 | 15 | 16 | 17 | 18 | describe('PrometheusParser.ts', () => { 19 | const parser = new PrometheusParser(); 20 | test('parse', () => { 21 | const result = parser.parse(prometheusStr); 22 | console.log(TD.stringify(result, " ")); 23 | expect(result).toMatchSnapshot(); 24 | }) 25 | }); -------------------------------------------------------------------------------- /src/models/TreeState.spec.ts: -------------------------------------------------------------------------------- 1 | import TreeState from './TreeState'; 2 | import sampleData from '../sampleData'; 3 | import { describe, expect, test } from 'vitest' 4 | 5 | describe('TreeState.ts', ()=> { 6 | test('JsonString', () => { 7 | const state = new TreeState(sampleData.jsonStr); 8 | expect(state.tree).toBeDefined(); 9 | expect(state.selected?.key).toBe('root'); 10 | expect(state.selection).toEqual({}); 11 | expect(state.history.items.length).toBe(1); 12 | 13 | state.select('invalidPath'); 14 | expect(state.selected?.key).toBe('root'); 15 | 16 | state.select('#/activityHistory/1'); 17 | expect(state.selected?.key).toBe('1'); 18 | expect(state.selection.start?.pos).toBe(339); 19 | expect(state.selection.end?.pos).toBe(582); 20 | expect(state.history.items.length).toBe(2); 21 | 22 | state.toggleMaxPane('table'); 23 | expect(state.maxPane).toBe('table'); 24 | state.toggleMaxPane('table'); 25 | expect(state.maxPane).toBe(''); 26 | }); 27 | }) -------------------------------------------------------------------------------- /src/parsers/YAMLParserPlugin.spec.ts: -------------------------------------------------------------------------------- 1 | import sampleData from '../sampleData'; 2 | import { describe, expect, test } from 'vitest' 3 | import YAMLParserPlugin from './YAMLParserPlugin'; 4 | import { TD, TDEncodeOption } from 'treedoc'; 5 | 6 | describe('YAMLParser.ts', () => { 7 | const parser = new YAMLParserPlugin(); 8 | test('looksLike', () => { 9 | expect(parser.looksLike(sampleData.yamlStr)).toBeTruthy(); 10 | expect(parser.looksLike(sampleData.xmlStr)).toBeFalsy(); 11 | expect(parser.looksLike(sampleData.jsonStr)).toBeFalsy(); 12 | }); 13 | 14 | test('parse', () => { 15 | const result = parser.parse(sampleData.yamlStr); 16 | expect(result.message).toMatchInlineSnapshot('"YAML.parse()"'); 17 | const encodeOpt = new TDEncodeOption(); 18 | encodeOpt.coderOption.coders = []; // remote default coder which will use `toJSON()` method 19 | encodeOpt.jsonOption.quoteChar = "'"; 20 | expect(TD.stringify(result.result, encodeOpt)).toMatchSnapshot(); 21 | }); 22 | }); 23 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | jest: true, 6 | }, 7 | extends: [ 8 | 'plugin:vue/essential', 9 | '@vue/airbnb', 10 | ], 11 | rules: { 12 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 13 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 14 | 'max-len': ['error', { code: 200, tabWidth: 2 }], 15 | curly: ['off'], 16 | 'no-plusplus': ['off'], 17 | 'no-continue': ['off'], 18 | 'no-multi-spaces': ['off'], 19 | 'no-param-reassign': ['off'], 20 | 'no-underscore-dangle': ['off'], 21 | 'no-console': ['off'], 22 | 'no-restricted-syntax': [ 23 | 'error', 24 | 'ForInStatement', 25 | // 'ForOfStatement', // Allow ForOfStatement 26 | 'LabeledStatement', 27 | 'WithStatement', 28 | ], 29 | "vue/no-unused-components": ["warn", { 30 | "ignoreWhenBindingPresent": true 31 | }], 32 | }, 33 | parserOptions: { 34 | parser: 'babel-eslint', 35 | ecmaVersion: 7, 36 | }, 37 | }; 38 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "warning", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "linterOptions": { 7 | "exclude": [ 8 | "node_modules/**" 9 | ] 10 | }, 11 | "rules": { 12 | "no-trailing-whitespace": false, 13 | "align": [true, "statements"], 14 | "quotemark": [true, "single", "avoid-escape", "avoid-template"], 15 | "indent": [true, "spaces", 2], 16 | "interface-name": false, 17 | "no-consecutive-blank-lines": false, 18 | "object-literal-sort-keys": false, 19 | "ordered-imports": false, 20 | "no-console": false, 21 | "curly":false, 22 | "no-for-in-array":false, 23 | "max-line-length": [ 24 | true, 25 | { 26 | "limit": 150, 27 | "ignore-pattern": "^import [^,]+ from |^export | implements" 28 | } 29 | ], 30 | "radix":false, 31 | "object-literal-key-quotes": [true, "as-needed"], 32 | "member-access": false, 33 | "max-classes-per-file": false, 34 | "member-ordering": false, 35 | "arrow-parens": false, 36 | "no-eval": false 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/UrlParam.ts: -------------------------------------------------------------------------------- 1 | import { TD, TDJSONParser } from 'treedoc'; 2 | import { TableConfig } from './components/Vue2DataTable'; 3 | import { TDVOption } from './lib'; 4 | import TableParam from './models/TableParam'; 5 | 6 | export default class UrlParam { 7 | data: string | null; 8 | dataUrl: string | null; 9 | embeddedId: string | null; 10 | title: string; 11 | initialPath?: string | null; 12 | tableConfig?: TableConfig; 13 | option?: TDVOption; 14 | 15 | constructor() { 16 | const url = new URL(window.location.href); 17 | this.dataUrl = url.searchParams.get('dataUrl'); 18 | this.data = url.searchParams.get('data'); 19 | this.embeddedId = url.searchParams.get('embeddedId'); 20 | this.initialPath = url.searchParams.get('initialPath'); 21 | this.title = url.searchParams.get('title') || 'Treedoc Viewer'; 22 | 23 | const tableConfigStr = url.searchParams.get('tableConfig'); 24 | if (tableConfigStr) 25 | this.tableConfig = TD.parse(tableConfigStr); 26 | 27 | const optStr = url.searchParams.get('option'); 28 | if (optStr) 29 | this.option = TD.parse(optStr); 30 | 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import 'bootstrap/dist/css/bootstrap.css'; 2 | import 'bootstrap-vue/dist/bootstrap-vue.css'; 3 | import 'font-awesome/css/font-awesome.css'; 4 | 5 | import Vue from 'vue'; 6 | import VueRouter from 'vue-router'; 7 | import Datatable from 'vue2-datatable-component'; 8 | import BootstrapVue from 'bootstrap-vue'; 9 | import msplit from 'msplit'; 10 | import App from './App.vue'; 11 | import Home from './Home.vue'; 12 | import TableTest from './TableTest.vue'; 13 | import TreeViewItem from './components/TreeViewItem.vue'; 14 | import { codemirror } from 'vue-codemirror-lite'; 15 | import Util from './util/Util'; 16 | 17 | Vue.config.productionTip = false; 18 | Vue.use(Datatable); 19 | Vue.use(BootstrapVue); 20 | Vue.use(msplit); 21 | Vue.use(VueRouter); 22 | Vue.component('tree-view-item', TreeViewItem); 23 | Vue.component('codemirror', codemirror); 24 | Vue.filter('textLimit', Util.textLimit); 25 | Vue.filter('toFixed', Util.toFixed); 26 | 27 | const router = new VueRouter({ 28 | routes: [ 29 | { path: '/', component: Home }, 30 | { path: '/table', component: TableTest }, 31 | ], 32 | }); 33 | 34 | new Vue({ 35 | router, 36 | render: h => h(App), 37 | }).$mount('#app'); 38 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-present, Jianwu Chen 4 | Author/Developer: Jianwu Chen 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. -------------------------------------------------------------------------------- /src/parsers/PrometheusParserPlugin.ts: -------------------------------------------------------------------------------- 1 | import { ParserPlugin, ParseResult } from '../models/TDVOption'; 2 | import { CSVOption, TDNode, TDObjectCoder } from 'treedoc'; 3 | import PrometheusParser from './PrometheusParser'; 4 | 5 | export class PrometheusParserOption { 6 | } 7 | 8 | export default class PrometheusParserPlugin implements ParserPlugin { 9 | syntax = 'csv'; 10 | option: PrometheusParserOption = {}; 11 | 12 | constructor(public name = 'Prometheus', public fieldSep = ',') {} 13 | 14 | looksLike(str: string): boolean { 15 | return str.indexOf('\n# HELP ') >= 0 && str.indexOf('\n# TYPE ') >= 0 16 | } 17 | 18 | get csvOption(): CSVOption { 19 | return new CSVOption().setFieldSep(this.fieldSep); 20 | } 21 | 22 | parse(str: string): ParseResult { 23 | const result = new ParseResult(); 24 | try { 25 | result.result = TDObjectCoder.get().encode(new PrometheusParser().parse(str)); 26 | result.message = 'PrometheusParser.parse()'; 27 | return result; 28 | } catch (e2) { 29 | result.message = `Error:${(e2 as any).message}`; 30 | console.error(e2); 31 | return result; 32 | } 33 | } 34 | 35 | stringify(obj: TDNode): string { 36 | return obj.toJSON(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /sample/embedded.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Open in a new windows | 4 | Open in a new windows with dataUrl | 5 | Open in a new windows with Data on URL (avoid pass confidential data) 6 | 7 | 8 | 27 | -------------------------------------------------------------------------------- /src/models/TreeUtil.ts: -------------------------------------------------------------------------------- 1 | import { TDNode, TDNodeType } from 'treedoc'; 2 | 3 | export default class TreeUtil { 4 | static readonly KEY_TYPE = '$type'; 5 | static readonly KEY_ID = '$id'; 6 | static readonly KEY_REF = '$ref'; 7 | 8 | static getSimpleTypeName(typeName: string): string { 9 | const p = typeName.indexOf('<'); // remove generic types 10 | if (p > 0) 11 | typeName = typeName.substr(0, p); 12 | const p2 = typeName.lastIndexOf('.'); 13 | return p2 < 0 ? typeName : typeName.substring(p2 + 1); 14 | } 15 | 16 | static getTypeLabel(node: TDNode) { 17 | let t = null; // TODO: support type factory 18 | const type = node.getChildValue(TreeUtil.KEY_TYPE); 19 | if (type && typeof(type) === 'string') 20 | t = type; 21 | if (!t) 22 | return ''; 23 | return TreeUtil.getSimpleTypeName(t); 24 | } 25 | 26 | static getTypeSizeLabel(node: TDNode, includeSummary = false) { 27 | let label = node.type === TDNodeType.ARRAY ? `[${node.getChildrenSize()}]` : `{${node.getChildrenSize()}}`; 28 | let tl = this.getTypeLabel(node); 29 | const id = node.getChildValue(TreeUtil.KEY_ID); 30 | if (id) 31 | tl += `@${id}`; 32 | 33 | if (tl.length > 0) // Special handling for type and hash 34 | label += ` <${tl}>`; 35 | 36 | if (includeSummary) 37 | label = node.toStringInternal(label, false, false, 100); 38 | 39 | return label; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/DataFilter.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { DataTableOptions, JS_QUERY_DEFAULT } from './Vue2DataTable'; 3 | import { TDNode } from 'treedoc'; 4 | import { TableUtil } from '../models/TableUtil'; 5 | 6 | 7 | export default { 8 | filter(opt: DataTableOptions) { 9 | opt.filteredData = opt.rawData.filter(r => opt.query.match(r)); 10 | 11 | if (opt.query.jsQuery && opt.query.jsQuery !== JS_QUERY_DEFAULT) { 12 | const func = `$=> ${opt.query.jsQuery}`; 13 | try { 14 | const filterFunc = eval(func) 15 | opt.filteredData = opt.filteredData.filter(r => filterFunc(TableUtil.rowToMapWithAllFields(r))); 16 | } catch(e) { 17 | // When run in chrome extension, eval is not allowed 18 | console.error(`Error evaluate JSQuery:${func}`); 19 | console.error(e) 20 | } 21 | } 22 | 23 | const q = opt.query; 24 | opt.total = opt.filteredData.length; 25 | if (q.offset >= opt.total) 26 | q.offset = Math.max(0, opt.total - q.limit); 27 | if (opt.query.sort) { 28 | const getFieldValue = (row: any) => { 29 | let v = row[q.sort!]; 30 | v = v instanceof TDNode && v.value !== undefined ? v.value : v; 31 | return v || ''; 32 | }; 33 | 34 | opt.filteredData = _.orderBy(opt.filteredData , getFieldValue, q.order); 35 | } 36 | const end = (q.offset === undefined || !q.limit) ? undefined : q.offset + q.limit; 37 | return opt.data = opt.filteredData.slice(q.offset, end); 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /src/components/JsonPath.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 46 | 52 | -------------------------------------------------------------------------------- /src/models/TDVOption.ts: -------------------------------------------------------------------------------- 1 | import { Component } from 'vue'; 2 | import { TDNode } from 'treedoc'; 3 | import { DataTableOptions, Query } from '../components/Vue2DataTable'; 4 | 5 | export enum ParseStatus { 6 | SUCCESS, 7 | WARN, 8 | ERROR, 9 | } 10 | 11 | export class ParseResult { 12 | result?: TDNode; 13 | status = ParseStatus.SUCCESS; 14 | message = ''; 15 | } 16 | 17 | export interface ParserPlugin { 18 | name: string; 19 | syntax: string; 20 | option: TOpt; 21 | 22 | looksLike(str: string): boolean; 23 | parse(str: string): ParseResult; 24 | stringify(obj: TDNode): string; 25 | configComp?: Component; 26 | } 27 | 28 | export default class TDVOptions { 29 | maxPane?: string; 30 | textWrap?: boolean; 31 | showTable?: boolean; 32 | showSource?: boolean; 33 | showTree?: boolean; 34 | 35 | parsers?: ParserPlugin[]; 36 | 37 | // If pattern is string, it will use wildcard matching 38 | tableOptRules?: {pattern: RegExp | string, opt: DataTableOptions}; 39 | defaultTableOpt?: DataTableOptions = { 40 | // fixHeaderAndSetBodyMaxHeight: 200, 41 | // tblStyle: 'table-layout: fixed', // must 42 | tblClass: 'table-bordered', 43 | pageSizeOptions: [5, 20, 50, 100, 200, 500], 44 | columns: [], 45 | data: [], 46 | filteredData: [], 47 | // filteredDataAsObjectArray: [], 48 | rawData: [], 49 | total: 0, 50 | query: new Query(), 51 | xprops: { tstate: null }, 52 | }; 53 | 54 | setParsers(parsers?: ParserPlugin[]) { 55 | this.parsers = parsers; 56 | return this; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/lib.ts: -------------------------------------------------------------------------------- 1 | import 'bootstrap/dist/css/bootstrap.css'; 2 | import 'bootstrap-vue/dist/bootstrap-vue.css'; 3 | import 'font-awesome/css/font-awesome.css'; 4 | 5 | import Vue from 'vue'; 6 | import BootstrapVue from 'bootstrap-vue'; 7 | import Datatable from 'vue2-datatable-component'; 8 | import msplit from 'msplit'; 9 | import { codemirror } from 'vue-codemirror-lite'; 10 | 11 | 12 | import JsonTreeTable from './components/JsonTreeTable.vue'; 13 | import JsonTable from './components/JsonTable.vue'; 14 | 15 | import TreeState from './models/TreeState'; 16 | import TreeViewItem from './components/TreeViewItem.vue'; 17 | import JSONParserPlugin, {JSONParserType, JSONParserOption} from './parsers/JSONParserPlugin'; 18 | import YAMLParserPlugin from './parsers/YAMLParserPlugin'; 19 | import XMLParserPlugin from './parsers/XMLParserPlugin'; 20 | import CSVParserPlugin from './parsers/CSVParserPlugin'; 21 | import TDVOption from './models/TDVOption'; 22 | import Util from './util/Util'; 23 | 24 | 25 | export default { 26 | install(vue: typeof Vue) { 27 | vue.use(BootstrapVue); 28 | vue.use(Datatable); 29 | Vue.use(msplit); 30 | Vue.component('codemirror', codemirror); 31 | vue.component('json-tree-table', JsonTreeTable); 32 | vue.component('json-table', JsonTable); 33 | Vue.component('tree-view-item', TreeViewItem); 34 | Vue.filter('textLimit', Util.textLimit); 35 | Vue.filter('toFixed', Util.toFixed); 36 | }, 37 | }; 38 | 39 | export { 40 | JsonTreeTable, 41 | TreeState, 42 | JSONParserPlugin, 43 | JSONParserOption, 44 | JSONParserType, 45 | YAMLParserPlugin, 46 | XMLParserPlugin, 47 | CSVParserPlugin, 48 | TDVOption, 49 | }; 50 | -------------------------------------------------------------------------------- /src/parsers/CSVParserPlugin.ts: -------------------------------------------------------------------------------- 1 | import { ParserPlugin, ParseResult } from '../models/TDVOption'; 2 | import { CSVParser, CSVOption, CSVWriter, TDNode } from 'treedoc'; 3 | import Util from '../util/Util'; 4 | 5 | export class CSVParserOption { 6 | } 7 | 8 | export default class CSVParserPlugin implements ParserPlugin { 9 | syntax = 'csv'; 10 | option: CSVParserOption = {}; 11 | 12 | constructor(public name = 'CSV', public fieldSep = ',') {} 13 | 14 | looksLike(str: string): boolean { 15 | const topLines = Util.topLines(str, 5000); 16 | if (topLines.numLines <= 1) 17 | return false; // Single line 18 | try { 19 | const node = CSVParser.get().parse(str.substr(0, topLines.length), this.csvOption); 20 | const columnSize = node.children![0].getChildrenSize(); 21 | if (columnSize < 2) 22 | return false; 23 | for (let row = 1; row < node.getChildrenSize(); row++) { 24 | if (node.children![row].getChildrenSize() !== columnSize) 25 | return false; 26 | } 27 | return true; 28 | } catch (e) { 29 | return false; 30 | } 31 | return false; 32 | } 33 | 34 | get csvOption(): CSVOption { 35 | return new CSVOption().setFieldSep(this.fieldSep); 36 | } 37 | 38 | parse(str: string): ParseResult { 39 | const result = new ParseResult(); 40 | try { 41 | result.result = CSVParser.get().parse(str, this.csvOption); 42 | result.message = 'CSVParser.parse()'; 43 | return result; 44 | } catch (e2) { 45 | result.message = `Error:${(e2 as any).message}`; 46 | console.error(e2); 47 | return result; 48 | } 49 | } 50 | 51 | stringify(obj: TDNode): string { 52 | return CSVWriter.get().writeAsString(obj, this.csvOption); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "treedoc-viewer", 3 | "version": "0.1.71", 4 | "main": "./dist/lib/lib.common.js", 5 | "typings": "./dist/lib/lib.d.ts", 6 | "files": [ 7 | "dist/lib/*", 8 | "src/*", 9 | "public/*", 10 | "*.json", 11 | "*.js" 12 | ], 13 | "scripts": { 14 | "test": "vitest", 15 | "coverage": "vitest run --coverage", 16 | "serve": "NODE_OPTIONS=--openssl-legacy-provider vue-cli-service serve", 17 | "build-app": "vue-cli-service build --dest dist/app", 18 | "build-lib": "vue-cli-service build --dest dist/lib --target lib --name lib src/lib.ts", 19 | "build": "vitest --run && yarn build-app && yarn build-lib", 20 | "deploy": "yarn build-app && gh-pages -d dist/app && yarn gh-pages -d dist/app -r git@github.com:treedoc/treedoc.github.io.git -b master", 21 | "lint": "vue-cli-service lint" 22 | }, 23 | "dependencies": { 24 | "bootstrap-vue": "^2.22.0", 25 | "font-awesome": "^4.7.0", 26 | "jquery": "^3.5.1", 27 | "jsdom": "^20.0.0", 28 | "lodash": "^4.17.21", 29 | "msplit": "0.1.24", 30 | "treedoc": "^0.3.44", 31 | "ts-node": "~10.7.0", 32 | "vue": "^2.6.11", 33 | "vue-class-component": "^7.2.3", 34 | "vue-codemirror-lite": "^1.0.4", 35 | "vue-property-decorator": "9.0.0", 36 | "vue-router": "^3.4.6", 37 | "vue2-datatable-component": "https://github.com/TrueCarry/vue2-datatable", 38 | "yaml": "^1.10.0", 39 | "yarn": "^1.22.22" 40 | }, 41 | "devDependencies": { 42 | "@types/lodash": "^4.14.182", 43 | "@types/yaml": "^1.9.7", 44 | "@vue/cli-plugin-typescript": "^4.5.17", 45 | "@vue/cli-service": "^4.5.17", 46 | "@vue/test-utils": "^1.0.3", 47 | "gh-pages": "^3.1.0", 48 | "handlebars": "^4.7.6", 49 | "tslint": "^6.1.3", 50 | "typescript": "~4.6.4", 51 | "vitest": "^0.23.4", 52 | "vue-cli-plugin-e2e-webdriverio": "^2.0.3", 53 | "vue-template-compiler": "^2.6.12" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/models/TableState.ts: -------------------------------------------------------------------------------- 1 | import { TDNode, TreeDoc, Bookmark, TDObjectCoder, TDNodeType, JSONPointer } from 'treedoc'; 2 | import History from './History'; 3 | import { ParserPlugin, ParseStatus } from './TDVOption'; 4 | import JSONParserPlugin from '../parsers/JSONParserPlugin'; 5 | import { Query, Column, DataTableOptions } from '@/components/Vue2DataTable'; 6 | import { TreeState } from '@/lib'; 7 | import Lazy from './Lazy'; 8 | 9 | // State associated with particular node 10 | export class TableNodeState { 11 | constructor( 12 | public query: Query, 13 | public expandedLevel: number, 14 | public columns: Column[], 15 | public isColumnExpanded: boolean) { } 16 | } 17 | 18 | export class TableOptionRule { 19 | constructor( 20 | public pattern: RegExp | string, 21 | public opt: DataTableOptions) { } 22 | } 23 | 24 | export class TDVTableOption { 25 | tableOptionRules: TableOptionRule[] = []; 26 | defTableOpt: DataTableOptions = { 27 | // fixHeaderAndSetBodyMaxHeight: 200, 28 | // tblStyle: 'table-layout: fixed', // must 29 | tblClass: 'table-bordered', 30 | pageSizeOptions: [5, 20, 50, 100, 200, 500], 31 | columns: [], 32 | data: [], 33 | filteredData: [], 34 | // filteredDataAsObjectArray: [], 35 | rawData: [], 36 | total: 0, 37 | query: new Query(), 38 | xprops: { tstate: null, columnStatistic: {} }, 39 | }; 40 | 41 | } 42 | 43 | export default class TableState { 44 | // _rawData = new Lazy(); // the full dataset related to current node 45 | filteredData: any[] | null = null; // the data after filtering 46 | sortedData: any[] | null = null; // the sorted data 47 | hasTree = false; // If there's tree widget in the cells 48 | 49 | constructor( 50 | public treeState: TreeState, 51 | public nodeState: TableNodeState, 52 | public tableOpt: TDVTableOption) { 53 | } 54 | 55 | // buildDataTableOption() { 56 | // } 57 | 58 | // private get rawData() { 59 | // return this._rawData.get(() => { 60 | // return []; 61 | // }; 62 | // } 63 | } 64 | -------------------------------------------------------------------------------- /src/util/Util.ts: -------------------------------------------------------------------------------- 1 | export default class Util { 2 | static nonBlankStartsWith(str: string, pattern: string[], detectLength = 1000): boolean { 3 | for (let i = 0; i < detectLength && i < str.length; i++) { 4 | if (this.isBlank(str[i])) 5 | continue; 6 | for (const p of pattern) { 7 | if (str.startsWith(p, i)) 8 | return true; 9 | } 10 | return false; 11 | } 12 | return false; 13 | } 14 | 15 | static nonBlankEndsWith(str: string, pattern: string[], detectLength= 1000): boolean { 16 | for (let i = str.length - 1; i >= 0 && i > str.length - detectLength; i--) { 17 | if (this.isBlank(str[i])) 18 | continue; 19 | for (const p of pattern) { 20 | if (str.endsWith(p, i + 1)) 21 | return true; 22 | } 23 | return false; 24 | } 25 | return false; 26 | } 27 | 28 | static isBlank(str: string): boolean { 29 | return ' \n\r\t'.indexOf(str) >= 0; 30 | } 31 | 32 | /** Read head string in str upto length, and terminate the at the last line break within this head string */ 33 | static topLines(str: string, length: number): {length: number, numLines: number} { 34 | const result = { length: -1, numLines: 0 }; 35 | for (let i = Math.min(str.length, length); i >= 0; i--) { 36 | if (str[i] === '\n') { 37 | if (result.length < 0) 38 | result.length = i + 1; 39 | result.numLines ++; 40 | } 41 | } 42 | return result; 43 | } 44 | 45 | public static doIf(condition: boolean, action: () => void) { if (condition) action(); } 46 | 47 | public static textLimit(text: string, limit: number, suffix = '...'): string { 48 | if (typeof text !== 'string') 49 | text = JSON.stringify(text); 50 | if (!text || text.length <= limit) 51 | return text; 52 | return text.substring(0, limit) + suffix; 53 | } 54 | 55 | public static toFixed(value: number, precision: number): string { 56 | const power = Math.pow(10, precision || 0); 57 | return String(Math.round(value * power) / power); 58 | } 59 | 60 | public static head(array: any[], n = 1) { return array.slice(0, n); } 61 | 62 | } 63 | -------------------------------------------------------------------------------- /src/components/TreeView.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 61 | 62 | 72 | -------------------------------------------------------------------------------- /src/TableTest.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 74 | 75 | 77 | -------------------------------------------------------------------------------- /src/components/SimpleValue.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 71 | -------------------------------------------------------------------------------- /src/components/td-Value.vue: -------------------------------------------------------------------------------- 1 | 19 | 70 | > 84 | -------------------------------------------------------------------------------- /src/parsers/JSONParserPlugin.ts: -------------------------------------------------------------------------------- 1 | import { ParserPlugin, ParseResult, ParseStatus } from '../models/TDVOption'; 2 | import { TDJSONParser, TDJSONParserOption, TDNodeType, TDNode, TDJSONWriter, TDJSONWriterOption, StringCharSource, TreeDoc } from 'treedoc'; 3 | import YAMLParserPlugin from './YAMLParserPlugin'; 4 | import Util from '../util/Util'; 5 | 6 | export class JSONParserOption { 7 | } 8 | 9 | export enum JSONParserType { 10 | NORMAL, 11 | JAVA_MAP_TO_STRING, 12 | LOMBOK_TO_STRING, 13 | } 14 | 15 | export default class JSONParserPlugin implements ParserPlugin { 16 | syntax = 'json'; 17 | option: JSONParserOption = {}; 18 | 19 | constructor( 20 | public name = 'JSON/JSONEX', 21 | public type = JSONParserType.NORMAL) { 22 | } 23 | 24 | looksLike(str: string): boolean { 25 | if (new YAMLParserPlugin().looksLike(str)) 26 | return false; 27 | if (str.length < 1000000 && this.parse(str).status !== ParseStatus.SUCCESS) 28 | return false; 29 | const opt = this.getTDJSONParserOption(this.type); 30 | if (!(Util.nonBlankEndsWith(str, [opt.deliminatorObjectEnd, opt.deliminatorArrayEnd]))) 31 | return false; 32 | let pColon = str.indexOf(':'); 33 | let pEqual = str.indexOf('='); 34 | pColon = pColon < 0 ? Number.MAX_SAFE_INTEGER : pColon; 35 | pEqual = pEqual < 0 ? Number.MAX_SAFE_INTEGER : pEqual; 36 | if (pColon > pEqual) 37 | return this.type === JSONParserType.JAVA_MAP_TO_STRING || this.type === JSONParserType.LOMBOK_TO_STRING; 38 | return true; 39 | } 40 | 41 | parse(str: string): ParseResult { 42 | const result = new ParseResult(); 43 | try { 44 | const src = new StringCharSource(str); 45 | const nodes: TDNode[] = []; 46 | const opt = this.getTDJSONParserOption(this.type); 47 | opt.setDefaultRootType(TDNodeType.MAP); 48 | while (src.skipSpacesAndReturnsAndCommas()) 49 | nodes.push(TDJSONParser.get().parse(src, opt)); 50 | result.result = nodes.length === 1 ? nodes[0] : TreeDoc.merge(nodes).root; 51 | result.message = 'TDJSONParser.parse()'; 52 | return result; 53 | } catch (e2) { 54 | result.message = `Error:${(e2 as any).message}`; 55 | result.status = ParseStatus.ERROR; 56 | console.error(e2); 57 | return result; 58 | } 59 | } 60 | 61 | private getTDJSONParserOption(type: JSONParserType) { 62 | switch(type) { 63 | case JSONParserType.JAVA_MAP_TO_STRING: return TDJSONParserOption.ofMapToString(); 64 | case JSONParserType.LOMBOK_TO_STRING: return new TDJSONParserOption().setDeliminatorKey('=').setDeliminatorObject('(', ')'); 65 | default: return new TDJSONParserOption(); 66 | } 67 | } 68 | 69 | 70 | stringify(obj: TDNode): string { 71 | return TDJSONWriter.get().writeAsString(obj, new TDJSONWriterOption().setIndentFactor(2)); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/parsers/__snapshots__/PrometheusParser.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1 2 | 3 | exports[`PrometheusParser.ts > parse 1`] = ` 4 | { 5 | "go_gc_duration_seconds": Metric { 6 | "help": "A summary of the pause duration of garbage collection cycles.", 7 | "metricsValues": [ 8 | MetricValue { 9 | "count": 0, 10 | "quantiles": { 11 | "0": 0, 12 | "0.25": 0, 13 | "0.5": 0, 14 | "0.75": 0, 15 | "1": 0, 16 | }, 17 | "sum": 0, 18 | }, 19 | ], 20 | "type": "summary", 21 | }, 22 | "http_request_duration_seconds": Metric { 23 | "help": "A histogram of the request duration.", 24 | "metricsValues": [ 25 | MetricValue { 26 | "$$bucket": { 27 | "+Inf": 144320, 28 | "0.05": 24054, 29 | "0.1": 33444, 30 | "0.2": 100392, 31 | "0.5": 129389, 32 | "1": 133988, 33 | }, 34 | "count": 144320, 35 | "sum": 53423, 36 | }, 37 | ], 38 | "type": "histogram", 39 | }, 40 | "http_requests_total": Metric { 41 | "help": "The total number of HTTP requests.", 42 | "metricsValues": [ 43 | MetricValue { 44 | "code": "200", 45 | "method": "post", 46 | "value": 1027, 47 | }, 48 | MetricValue { 49 | "code": "400", 50 | "method": "post", 51 | "value": 3, 52 | }, 53 | ], 54 | "type": "counter", 55 | }, 56 | "metric_without_timestamp_and_labels": Metric { 57 | "help": "The total number of HTTP requests.", 58 | "metricsValues": [ 59 | MetricValue { 60 | "value": 12.47, 61 | }, 62 | ], 63 | "type": "counter", 64 | }, 65 | "msdos_file_access_time_seconds": Metric { 66 | "help": "", 67 | "metricsValues": [ 68 | MetricValue { 69 | "value": 1458255915, 70 | }, 71 | ], 72 | "type": "count", 73 | }, 74 | "rpc_duration_seconds": Metric { 75 | "help": "A summary of the RPC duration in seconds.", 76 | "metricsValues": [ 77 | MetricValue { 78 | "count": 2693, 79 | "quantiles": { 80 | "0.01": 3102, 81 | "0.05": 3272, 82 | "0.5": 4773, 83 | "0.9": 9001, 84 | "0.99": 76656, 85 | }, 86 | "sum": 17560473, 87 | }, 88 | ], 89 | "type": "summary", 90 | }, 91 | "something_weird": Metric { 92 | "help": "", 93 | "metricsValues": [ 94 | MetricValue { 95 | "value": NaN, 96 | }, 97 | ], 98 | "type": "count", 99 | }, 100 | "thanos_objstore_bucket_operation_duration_seconds": Metric { 101 | "help": "Duration of successful operations against the bucket", 102 | "metricsValues": [ 103 | MetricValue { 104 | "$$bucket": { 105 | "0.01": 0, 106 | "0.1": 0, 107 | }, 108 | "bucket": "thanos", 109 | "operation": "attributes", 110 | }, 111 | ], 112 | "type": "histogram", 113 | }, 114 | } 115 | `; 116 | -------------------------------------------------------------------------------- /src/components/ExpandControl.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 89 | 97 | -------------------------------------------------------------------------------- /src/parsers/__snapshots__/YAMLParserPlugin.spec.ts.snap: -------------------------------------------------------------------------------- 1 | // Vitest Snapshot v1 2 | 3 | exports[`YAMLParser.ts > parse 2`] = `"{'doc':{'idMap':{},'root':{'$ref':'../../'},'$id':1},'key':'root','type':1,'deduped':false,'tData':{},'children':[{'doc':{'$ref':'#1'},'key':'0','type':0,'deduped':false,'tData':{},'parent':{'$ref':'../../../'},'children':[{'doc':{'$ref':'#1'},'key':'martin','type':0,'deduped':false,'tData':{},'parent':{'$ref':'../../../'},'children':[{'doc':{'$ref':'#1'},'key':'name','type':2,'mValue':'Martin D\\\\'vloper','deduped':false,'tData':{},'parent':{'$ref':'../../../'},'start':{'line':2,'col':10,'pos':22},'end':{'line':2,'col':25,'pos':37}},{'doc':{'$ref':'#1'},'key':'job','type':2,'mValue':'Developer','deduped':false,'tData':{},'parent':{'$ref':'../../../'},'start':{'line':3,'col':9,'pos':47},'end':{'line':3,'col':18,'pos':56}},{'doc':{'$ref':'#1'},'key':'skills','type':1,'deduped':false,'tData':{},'parent':{'$ref':'../../../'},'children':[{'doc':{'$ref':'#1'},'key':'0','type':2,'mValue':'python','deduped':false,'tData':{},'parent':{'$ref':'../../../'},'start':{'line':5,'col':8,'pos':77},'end':{'line':5,'col':14,'pos':83}},{'doc':{'$ref':'#1'},'key':'1','type':2,'mValue':'perl','deduped':false,'tData':{},'parent':{'$ref':'../../../'},'start':{'line':6,'col':8,'pos':92},'end':{'line':6,'col':12,'pos':96}},{'doc':{'$ref':'#1'},'key':'2','type':2,'mValue':'pascal','deduped':false,'tData':{},'parent':{'$ref':'../../../'},'start':{'line':7,'col':8,'pos':105},'end':{'line':7,'col':14,'pos':111}}],'start':{'line':5,'col':6,'pos':75},'end':{'line':8,'col':0,'pos':112}}],'start':{'line':2,'col':4,'pos':16},'end':{'line':8,'col':0,'pos':112}}],'start':{'line':1,'col':3,'pos':4},'end':{'line':8,'col':0,'pos':112}},{'doc':{'$ref':'#1'},'key':'1','type':0,'deduped':false,'tData':{},'parent':{'$ref':'../../../'},'children':[{'doc':{'$ref':'#1'},'key':'tabitha','type':0,'deduped':false,'tData':{},'parent':{'$ref':'../../../'},'children':[{'doc':{'$ref':'#1'},'key':'name','type':2,'mValue':'Tabitha Bitumen','deduped':false,'tData':{},'parent':{'$ref':'../../../'},'start':{'line':9,'col':10,'pos':134},'end':{'line':9,'col':25,'pos':149}},{'doc':{'$ref':'#1'},'key':'job','type':2,'mValue':'Developer','deduped':false,'tData':{},'parent':{'$ref':'../../../'},'start':{'line':10,'col':9,'pos':159},'end':{'line':10,'col':18,'pos':168}},{'doc':{'$ref':'#1'},'key':'skills','type':1,'deduped':false,'tData':{},'parent':{'$ref':'../../../'},'children':[{'doc':{'$ref':'#1'},'key':'0','type':2,'mValue':'lisp','deduped':false,'tData':{},'parent':{'$ref':'../../../'},'start':{'line':12,'col':8,'pos':189},'end':{'line':12,'col':12,'pos':193}},{'doc':{'$ref':'#1'},'key':'1','type':2,'mValue':'fortran','deduped':false,'tData':{},'parent':{'$ref':'../../../'},'start':{'line':13,'col':8,'pos':202},'end':{'line':13,'col':15,'pos':209}},{'doc':{'$ref':'#1'},'key':'2','type':2,'mValue':'erlang','deduped':false,'tData':{},'parent':{'$ref':'../../../'},'start':{'line':15,'col':0,'pos':218},'end':{'line':15,'col':0,'pos':224}}],'start':{'line':12,'col':6,'pos':187},'end':{'line':15,'col':0,'pos':225}}],'start':{'line':9,'col':4,'pos':128},'end':{'line':15,'col':0,'pos':225}}],'start':{'line':8,'col':3,'pos':115},'end':{'line':15,'col':0,'pos':225}}],'start':{'line':1,'col':0,'pos':1},'end':{'line':15,'col':0,'pos':225}}"`; 4 | -------------------------------------------------------------------------------- /src/models/TableUtil.ts: -------------------------------------------------------------------------------- 1 | import { CSVWriter, identity, ListUtil, TDNode, TDNodeType, TDObjectCoder } from 'treedoc'; 2 | import { DataTableOptions, Column, Query } from '../components/Vue2DataTable'; 3 | 4 | export class ColumnStatistic { 5 | total: number = 0; 6 | min: any; 7 | max: any; 8 | sum: number = 0; 9 | avg: number = 0; 10 | p50: number = 0; 11 | p90: number = 0; 12 | p99: number = 0; 13 | valueCounts: {[key: string]: number} = {} 14 | valueSortedByCounts: string[] = []; 15 | 16 | get valueCountsSorted(): {val: string, count: number, percent: number}[] { 17 | return this.valueSortedByCounts.map(key => ({val: key, count: this.valueCounts[key], percent: this.valueCounts[key]/this.total})); 18 | } 19 | } 20 | 21 | export class TableUtil { 22 | static rowsToObject(rows: any[], tableOpt: DataTableOptions): {[key: string]: any} { 23 | const result: any = {}; 24 | rows.forEach(row => result[row['@key']] = this.rowToObject(row, tableOpt)); 25 | return result; 26 | } 27 | 28 | static collectColumnStatistic(rows: any[], col: string): ColumnStatistic { 29 | console.log('collectColumnStatistic', col); 30 | const stat = new ColumnStatistic(); 31 | const vals: any[] = []; 32 | for (const row of rows) { 33 | stat.total++; 34 | let val = TableUtil.valToObject(row[col]); 35 | vals.push(val); 36 | if (val === undefined) // Skip undefined value 37 | val = ''; 38 | if (typeof val !== 'string' && typeof val !== 'number') { 39 | val = JSON.stringify(val); 40 | } 41 | if (stat.min === undefined || val < stat.min) 42 | stat.min = val; 43 | if (stat.max === undefined || val > stat.max) 44 | stat.max = val; 45 | if (typeof val === 'number') 46 | stat.sum += val; 47 | const key = '' + val; 48 | stat.valueCounts[key] = (stat.valueCounts[key] || 0) + 1; 49 | } 50 | vals.sort((a,b) => a - b); 51 | stat.avg = stat.sum / rows.length; 52 | if (stat.avg > 0) { // Calculate percentile only when avg is number 53 | stat.p50 = vals[Math.floor(vals.length * 0.5)] || 0; 54 | stat.p90 = vals[Math.floor(vals.length * 0.9)] || 0; 55 | stat.p99 = vals[Math.floor(vals.length * 0.99)] || 0; 56 | } 57 | stat.valueSortedByCounts = Object.keys(stat.valueCounts).sort((a, b) => stat.valueCounts[b] - stat.valueCounts[a]); 58 | return stat; 59 | } 60 | 61 | static rowToObject(row: any, tableOpt: DataTableOptions, includeKey = false, includeValue = true) { 62 | if (row['@value'] && !includeValue) 63 | return this.valToObject(row['@value']); 64 | 65 | const result: any = {}; 66 | for (const col of tableOpt.columns) { 67 | if (col.field === '#' || !col.visible) continue; 68 | if (col.field === '@key' && !includeKey) continue; 69 | result[col.field] = this.valToObject(row[col.field]); 70 | } 71 | return result; 72 | } 73 | 74 | static rowToMapWithAllFields(row: any) { 75 | return ListUtil.map(row, identity, this.valToObject); 76 | } 77 | 78 | static valToObject(val: any) { 79 | return val instanceof TDNode ? val.toObject(false, false) : val; 80 | } 81 | 82 | static toCSV(val: any) { 83 | const obj = TDObjectCoder.encode(val); 84 | return CSVWriter.instance.writeAsString(obj); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/components/Vue2DataTable.ts: -------------------------------------------------------------------------------- 1 | import { ColumnStatistic } from '@/models/TableUtil'; 2 | import { TD, TDNodeType } from 'treedoc'; 3 | 4 | export declare interface Column { 5 | field: string; 6 | visible?: boolean; 7 | html?: ((value: any, row: any) => string) | string; // 8 | [key: string]: any; 9 | } 10 | 11 | export class FieldQuery { 12 | query: string = ''; 13 | compiledQuery: string = ''; 14 | isRegex: boolean = false; 15 | // If it's array, the query string will be parsed with JSONex Parser as array of string 16 | // For example: "abc,def" will be parsed as ["abc", "def"], '"ab\"c", "def"' will be parsed as ['ab"c', 'def'] 17 | isArray: boolean = false; 18 | isNegate: boolean = false; 19 | 20 | queryCompiled?: (string | RegExp)[]; 21 | 22 | compile() { 23 | // Avoid recompile if query is not changes which will cause infinite update loop from vue 24 | if (this.query === this.compiledQuery) 25 | return; 26 | this.compiledQuery = this.query; 27 | 28 | if (!this.query) { 29 | this.queryCompiled = []; 30 | return; 31 | } 32 | const values: string[] = this.isArray ? TD.parse(this.query, {defaultRootType: TDNodeType.ARRAY}) : [this.query]; 33 | // console.log('compile', this.query, values); 34 | this.queryCompiled = values.map(q => { 35 | // console.log('compile', q) 36 | return this.isRegex ? new RegExp(q, 'i') : q.toString().toLowerCase(); 37 | }); 38 | } 39 | 40 | match(value: string): boolean { 41 | if (!this.query) 42 | return true; 43 | let result = false; 44 | for (const q of this.queryCompiled!) { 45 | if (q instanceof RegExp) { 46 | if (q.test(value)) { 47 | result = true; 48 | break; 49 | } 50 | } else { 51 | if (value.toLocaleLowerCase().indexOf(q) >= 0) { 52 | result = true; 53 | break; 54 | } 55 | } 56 | } 57 | return this.isNegate !== result; 58 | } 59 | } 60 | 61 | export const JS_QUERY_DEFAULT = '$'; 62 | export class Query { 63 | sort?: string; 64 | order?: boolean | 'asc' | 'desc'; 65 | offset: number = 0; 66 | limit: number = 100; 67 | jsQuery? = JS_QUERY_DEFAULT; 68 | // In Javascript map syntax: e.g. "{createdDate: $.created.date, nameUpper: $.name.toUpperCase()}" 69 | extendedFields?: string; 70 | fieldQueries: {[key:string]: FieldQuery} = {}; 71 | // [key: string]: any; 72 | 73 | constructor() { 74 | this.fieldQueries = {}; 75 | } 76 | 77 | match(row: any): boolean { 78 | Object.values(this.fieldQueries).forEach(fq => fq.compile()); 79 | for (const f of Object.keys(this.fieldQueries)) { 80 | const fq = this.fieldQueries[f]; 81 | if (!fq.match(`${row[f]}`)) 82 | return false; 83 | } 84 | return true; 85 | } 86 | } 87 | 88 | export declare interface TableConfig { 89 | Pagination?: boolean; 90 | fixHeaderAndSetBodyMaxHeight?: string | number; 91 | tblStyle?: string; 92 | tblClass?: string; 93 | pageSizeOptions?: number[]; 94 | columns: Column[]; 95 | } 96 | 97 | // https://onewaytech.github.io/vue2-datatable/doc/#/en/details/datatable-props 98 | export declare interface DataTableOptions extends TableConfig { 99 | data: any[]; 100 | total: number; 101 | query: Query; 102 | xprops: { [key: string]: any }; 103 | rawData: any[]; 104 | selection?: any[]; 105 | filteredData: any[]; 106 | // filteredDataAsObjectArray: any[]; 107 | // columnStatistic: {[key: string]: ColumnStatistcs}; 108 | } 109 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | GitHub Actions statusnpm 2 | 3 | # Treedoc Viewer 4 | 5 | A feature-rich viewer for Treedoc implemented with VueJS and typescript. Treedoc is an abstraction for all the tree-structured document formats such as JSON, YAML, XML. This viewer has built-in support of JSON/[JSONex](https://github.com/eBay/jsonex/blob/master/JSONEX.md), YAML and XML. It provides an easy way to plugin any other format by implementing the ParserPlugin interface. 6 | 7 | ## Features 8 | 9 | * Three views: Source, Tree and Table and they are toggleable 10 | * Flexible navigation 11 | * Back/forward navigation between tree nodes 12 | * Support breadcrumb view of the node path for easy navigation to parent nodes 13 | * Navigation through *$ref* node as defined in [OpenAPI](https://openapis.org/) or [Google Discovery Service](https://developers.google.com/discovery) 14 | * Navigation is synchronized between tree view and table view 15 | * Treeview 16 | * Support expand / collapse one level or all levels. 17 | * Tree is also embedded in table views 18 | * Table view (Based on [vue2-datatable-component](https://www.npmjs.com/package/vue2-datatable-component)) 19 | * Expand attributes for the child nodes as table columns 20 | * Support column filtering and sorting 21 | * Support pagination 22 | * Support column selection 23 | * Source View (Based on [CodeMirror](https://codemirror.net/)) 24 | * Syntax source highlighting 25 | * Synchronized highlighting in the source code when navigating through nodes 26 | * Support multiple file formats 27 | * different sources of the document: open local file, open URL or copy/paste 28 | * Auto-detect format to choose the right parser 29 | * Buildin Support following formats 30 | * JSONex format (extension of JSON) (Based on [treedoc](https://www.npmjs.com/package/treedoc)) 31 | * `text protobuf` which is support by JSONex parser 32 | * Custom format such as java `Map.toString`, java `Lombok.toString` 33 | * `Prometheus` text format. 34 | * `csv`, `tsv`, `ssv` (space-separated values) 35 | * `XML`, `Compact XML` 36 | * `YAML`, include multiple document format 37 | * Plugable parser, so that more format can be easily added. 38 | * Implemented as VueJS component, so it's easy to be reused in different applications 39 | 40 | ## Usage 41 | ### As a Vue component 42 | 43 | ```shell 44 | yarn add treedoc-viewer 45 | ``` 46 | 47 | ```js 48 | // main.js 49 | import TreedocViewer from 'treedoc-viewer' 50 | 51 | Vue.use(TreedocViewer); 52 | ``` 53 | 54 | In `public/index.html` 55 | ```html 56 | 57 | ``` 58 | 59 | In component template 60 | ```html 61 | 62 | ``` 63 | 64 | ### As embedded iframe 65 | If you are not using VueJs or don't want to introduce heavy dependencies, you can use embedded mode either through iframe or open a new window (tab) 66 | ```html 67 | 68 | 74 | ``` 75 | For a working example, please refer to [sample/embedded.html](https://github.com/treedoc/TreedocViewer/blob/master/sample/embedded.html) in github repo. 76 | 77 | ## Development 78 | ```shell 79 | yarn install 80 | yarn serve 81 | ``` 82 | 83 | ## Live Demo 84 | 85 | 86 | 87 | ## License 88 | 89 | Copyright 2019-2020 Jianwu Chen
90 | Author/Developer: Jianwu Chen 91 | 92 | Use of this source code is governed by an MIT-style license that can be found in the LICENSE file or at . 93 | -------------------------------------------------------------------------------- /src/parsers/YAMLParserPlugin.ts: -------------------------------------------------------------------------------- 1 | import YAML from 'yaml'; 2 | import { ParserPlugin, ParseResult } from '../models/TDVOption'; 3 | import { Bookmark, TD, TDNode, TDNodeType, TDObjectCoder, TreeDoc } from 'treedoc'; 4 | import XMLParserPlugin from './XMLParserPlugin'; 5 | import Util from '../util/Util'; 6 | import { Node, YAMLMap, Pair, YAMLSeq, Scalar } from 'yaml/types'; 7 | import { Type } from 'yaml/util'; 8 | import TextLine from '@/util/TextLine'; 9 | 10 | export class YMLParserOption { 11 | } 12 | 13 | export default class YAMLParserPlugin implements ParserPlugin { 14 | name = 'YAML'; 15 | syntax = 'yaml'; 16 | option: YMLParserOption = {}; 17 | 18 | looksLike(str: string): boolean { 19 | if (new XMLParserPlugin().looksLike(str)) 20 | return false; 21 | 22 | if (Util.nonBlankStartsWith(str, ['{', '[', '/'])) // Don't accept JSON 23 | return false; 24 | 25 | // A line aligned partial YAML from beginning is also a valid YAML file 26 | // JSON is not the case. That's how we guess if the file is a YAML instead of JSON. 27 | const topLines = Util.topLines(str, 5000); 28 | if (topLines.numLines <= 1) 29 | return false; // Single line 30 | 31 | try { 32 | YAML.parseAllDocuments(str.substring(0, topLines.length)); 33 | return true; 34 | } catch (e) { 35 | return false; 36 | } 37 | } 38 | 39 | parse(str: string): ParseResult { 40 | const result = new ParseResult(); 41 | try { 42 | // Not sure why some string accepted by parse(), but can't accepted by parseAllDocuments() 43 | // const doc = YAML.parseAllDocuments(str); 44 | // doc[0].cstNode 45 | 46 | result.result =this.parseYaml(str); 47 | result.message = 'YAML.parse()'; 48 | return result; 49 | } catch (e) { 50 | result.message = `Error:${(e as any).message}`; 51 | console.error(e); 52 | return result; 53 | } 54 | } 55 | 56 | private textLine?: TextLine; // Assume no concurrent parsing 57 | /** Try parse and parseAllDocuments */ 58 | parseYaml(str: string): any { 59 | this.textLine = new TextLine(str); 60 | const yaml = YAML.parseAllDocuments(str); 61 | if (!yaml[yaml.length-1].contents) { // Remote the last empty one 62 | yaml.splice(yaml.length - 1, 1); 63 | } 64 | const doc = new TreeDoc(); 65 | 66 | if (yaml.length === 1) { 67 | this.toTDNode(yaml[0].contents!, doc.root); 68 | } else { 69 | doc.root.type = TDNodeType.ARRAY; 70 | doc.root.children = yaml.map(y => this.toTDNode(y.contents!, doc.root.createChild())); 71 | } 72 | return doc.root; 73 | } 74 | 75 | toTDNode(yaml: Node | undefined, node: TDNode): TDNode { 76 | // console.log(TD.stringify(yaml)); 77 | // console.log(yaml.type); 78 | if (!yaml) 79 | return node; 80 | 81 | switch(yaml?.type) { 82 | case Type.FLOW_MAP: 83 | case Type.MAP: 84 | this.toTDNodeMap(yaml as YAMLMap, node); break; 85 | case Type.FLOW_SEQ: 86 | case Type.SEQ: 87 | this.toTDNodeAray(yaml as YAMLSeq, node); break; 88 | // Scala.Type 89 | case Type.PLAIN: 90 | case Type.BLOCK_FOLDED: 91 | case Type.BLOCK_LITERAL: 92 | case Type.PLAIN: 93 | case Type.QUOTE_DOUBLE: 94 | case Type.QUOTE_SINGLE: 95 | node.value = (yaml as Scalar).value; break; 96 | default: console.warn(`Unsupported type: ${yaml?.type}, ${TD.stringify(yaml)}, ${typeof yaml}, ${Object.keys(yaml)}`); 97 | } 98 | node.start = this.textLine!.getBookmark(yaml.range![0]); 99 | node.end = this.textLine!.getBookmark(yaml.range![1]); 100 | 101 | return node; 102 | } 103 | 104 | toTDNodeMap(yaml: YAMLMap, node: TDNode) { 105 | node.type = TDNodeType.MAP; 106 | for (const item of yaml.items as Pair[]) { 107 | const cNode = node.createChild(item.key.value) 108 | this.toTDNode(item.value, cNode); 109 | } 110 | return node; 111 | } 112 | 113 | toTDNodeAray(yaml: YAMLSeq, node: TDNode) { 114 | node.type = TDNodeType.ARRAY; 115 | for (const item of yaml.items) { 116 | const cNode = node.createChild(); 117 | this.toTDNode(item, cNode); 118 | } 119 | return node; 120 | } 121 | 122 | stringify(obj: any): string { 123 | return YAML.stringify(obj); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/parsers/XMLParserPlugin.ts: -------------------------------------------------------------------------------- 1 | import _ from 'lodash'; 2 | import { ParserPlugin, ParseResult } from '../models/TDVOption'; 3 | import { TDObjectCoder } from 'treedoc'; 4 | import Util from '../util/Util'; 5 | 6 | export class XMLParserOption { 7 | } 8 | 9 | interface XNode { 10 | type?: string; 11 | name?: string; 12 | tag?: string; 13 | value?: string | null; 14 | attr?: {[key: string]: string | null}; 15 | text?: string; 16 | children?: XNode []; 17 | } 18 | 19 | // when compiled with "typescript": "^3.9.7", it will throw error: Cannot find name 'DOMParserSupportedType'. so we redefine it here 20 | type DOMParserSupportedType = 'application/xhtml+xml' | 'application/xml' | 'image/svg+xml' | 'text/html' | 'text/xml'; 21 | 22 | export default class XMLParserPlugin implements ParserPlugin { 23 | option: XMLParserOption = {}; 24 | syntax = 'xml'; 25 | 26 | constructor( 27 | public name = 'XML', 28 | private mineType: DOMParserSupportedType = 'text/xml', 29 | private compact: boolean = false) {} 30 | 31 | looksLike(str: string): boolean { 32 | return Util.nonBlankStartsWith(str, ['<']) && Util.nonBlankEndsWith(str, ['>']); 33 | } 34 | 35 | parse(str: string): ParseResult { 36 | const result = new ParseResult(); 37 | try { 38 | const doc = new DOMParser().parseFromString(str.trim(), this.mineType); 39 | const root = doc.childNodes.length < 2 ? doc.childNodes[0] : doc; 40 | let xmlObj: XNode = this.docToObj(root); 41 | if (this.compact) 42 | xmlObj = { [xmlObj.tag || xmlObj.name!] : this.compactToObject(xmlObj)}; 43 | 44 | result.result = TDObjectCoder.get().encode(xmlObj); 45 | result.message = 'DOMParser().parseFromString()'; 46 | return result; 47 | } catch (e) { 48 | result.message = `Error:${(e as any).message}`; 49 | console.error(e); 50 | return result; 51 | } 52 | } 53 | 54 | docToObj(node: Node) { 55 | const result: XNode = {}; 56 | // if (!this.compact) 57 | result.type = node.constructor.name; 58 | if (node instanceof Element) { 59 | result.tag = node.tagName; 60 | if (node.getAttributeNames) { 61 | node.getAttributeNames().forEach(a => { 62 | result.attr = result.attr || {}; 63 | result.attr[a] = node.getAttribute(a); 64 | }); 65 | } 66 | // } else if (node instanceof Comment) { 67 | // result.text = node.textContent; 68 | // result.nodeValue = node.textContent; 69 | } else { 70 | result.name = node.nodeName; 71 | result.value = node.nodeValue; 72 | } 73 | 74 | if (node.childNodes) { 75 | node.childNodes.forEach(c => { 76 | if (c instanceof Text) { 77 | if (c.textContent && c.textContent.trim()) 78 | result.text = (result.text || '') + c.textContent; 79 | } else { 80 | result.children = result.children || []; 81 | result.children.push(this.docToObj(c)); 82 | } 83 | }); 84 | } 85 | return result; 86 | } 87 | 88 | private addToMap(map: any, key: string, val: any) { 89 | let vals = map[key]; 90 | if (!vals) { 91 | vals = [] as any[]; 92 | map[key] = vals; 93 | } 94 | vals.push(val); 95 | } 96 | 97 | compactToObject(n: XNode): any { 98 | if (!n.attr && !n.children) // Simple node 99 | return n.text; 100 | 101 | const map: {[key: string]: any[]} = {}; 102 | if (n.attr) { 103 | Object.keys(n.attr).forEach(key => { 104 | this.addToMap(map, key, n.attr![key]); 105 | }); 106 | } 107 | const comments: string[] = []; 108 | if (n.children) { 109 | n.children.forEach(c => { 110 | if (!c.tag) { // Assume it's comment 111 | console.log(`c.type=${c.type}, c.value=${c.value}, c.name=${c.name}`); 112 | if (c.type === 'ProcessingInstruction') 113 | this.addToMap(map, `?${c.name}`, c.value); 114 | else if (c.value) 115 | comments.push(c.value); 116 | else 117 | console.error('unknown node: ' + c); 118 | 119 | return; 120 | } 121 | 122 | let cnode: any = this.compactToObject(c); 123 | if (comments.length > 0) { 124 | if (_.isObject(cnode)) { 125 | // ts could narrow the type based on the _.isObject method signature by type predicate 126 | // (method) LoDashStatic.isObject(value?: any): value is object 127 | (cnode as any)['@comments'] = [...comments]; 128 | } else 129 | cnode = {'@comments': [...comments], '@val': cnode}; 130 | 131 | comments.length = 0; 132 | } 133 | this.addToMap(map, c.tag, cnode); 134 | }); 135 | } 136 | 137 | const res: {[key: string]: any} = {}; 138 | Object.keys(map).forEach(k => { 139 | res[k] = map[k].length === 1 ? map[k][0] : map[k]; 140 | }); 141 | if (n.text) 142 | res['@val'] = n.text; 143 | 144 | if (comments.length > 0) 145 | res['@comments'] = [...comments]; 146 | 147 | return res; 148 | } 149 | 150 | stringify(obj: any): string { 151 | return ''; 152 | } 153 | } 154 | -------------------------------------------------------------------------------- /src/components/SourceView.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 133 | 152 | -------------------------------------------------------------------------------- /src/Home.vue: -------------------------------------------------------------------------------- 1 | 26 | 27 | 102 | 103 | 140 | -------------------------------------------------------------------------------- /src/parsers/PrometheusParser.ts: -------------------------------------------------------------------------------- 1 | import { CharSource, LangUtil, ListUtil, StringCharSource } from 'treedoc' 2 | import _ from 'lodash'; 3 | 4 | const HELP = 'HELP'; 5 | const TYPE = 'TYPE'; 6 | export class Metrics { 7 | [key: string]: Metric; 8 | } 9 | 10 | export class Metric { 11 | help: string = ''; 12 | type: string = ''; 13 | metricsValues: MetricValue[] = []; 14 | 15 | getOrCreateValue(labels: {[key: string]: string}): MetricValue { 16 | for (const m of this.metricsValues) { 17 | if (m.matchLabels(labels)) 18 | return m; 19 | } 20 | const ret: MetricValue = new MetricValue(); 21 | Object.assign(ret, labels) 22 | this.metricsValues.push(ret); 23 | return ret; 24 | } 25 | } 26 | 27 | export class MetricValue { 28 | [key: string]: any; 29 | value?: number; 30 | quantiles?: {[key: string]: number}; 31 | // Use $$ prefix to avoid name conflict with a label of bucket 32 | $$bucket?: {[key: string]: number}; 33 | count?: number; 34 | sum?: number; 35 | 36 | addQuantile(key: string, value: number) { 37 | if (!this.quantiles) this.quantiles = {}; 38 | this.quantiles[key] = value 39 | } 40 | addBucket(key: string, value: number) { 41 | if (!this.$$bucket) this.$$bucket = {}; 42 | this.$$bucket[key] = value 43 | } 44 | 45 | matchLabels(labels: {[key: string]: string}): boolean { 46 | for(const [key, value] of Object.entries(labels)) { 47 | if (this[key] !== value) 48 | return false; 49 | } 50 | return true; 51 | } 52 | 53 | } 54 | 55 | export class MetricLine { 56 | constructor(public name: string) {}; 57 | labels: {[key: string]: string} = {}; 58 | value: number = 0; 59 | timestamp?: number; 60 | } 61 | 62 | export default class PrometheusParser { 63 | result: Metrics = {}; 64 | currentMetric: Metric = new Metric(); 65 | currentMetricKey: string = ''; 66 | 67 | parse(str: string) : Metrics { 68 | const src = new StringCharSource(str); 69 | while(src.skipSpacesAndReturns()) { 70 | if (src.peek() === '#') { 71 | src.skip(1); 72 | this.parseComment(src); 73 | } else { 74 | const metricLine = this.parseMetricLine(src); 75 | this.updateCurrentMetric(metricLine); 76 | } 77 | } 78 | return this.result; 79 | } 80 | 81 | parseComment(src: CharSource) { 82 | if (!src.skipSpacesAndReturns()) 83 | return; 84 | const help = this.parseCommentOfKey(src, HELP); 85 | if (help) 86 | this.getOrCreateMetric(help.name).help = help.value; 87 | else { 88 | const type = this.parseCommentOfKey(src, TYPE); 89 | if (type) 90 | this.getOrCreateMetric(type.name).type = type.value; 91 | } 92 | src.skipUntilTerminator('\n\r', true); 93 | } 94 | 95 | parseCommentOfKey(src: CharSource, key: string): {name: string, value: string} | null { 96 | if (src.startsWith(key)) { 97 | src.skip(key.length); 98 | src.skipChars(' '); 99 | const name = src.readUntilTerminator(' '); 100 | src.skipChars(' '); 101 | return {name, value: src.readUntilTerminator('\n\r')}; 102 | } 103 | return null; 104 | } 105 | 106 | parseMetricLine(src: CharSource): MetricLine { 107 | const ret: MetricLine = new MetricLine(src.readUntilTerminator('{ ')); 108 | if (src.peek() === '{') { 109 | src.skip(1); 110 | while(true) { 111 | const key = src.readUntilTerminator('=}'); 112 | if (src.read() === '}') break; // either = or } 113 | const quote = src.read(); 114 | if (quote !== '"' && quote !== '\'') throw src.createParseRuntimeException('missing quote when expecting a string label vale'); 115 | const val = src.readQuotedString(quote); 116 | ret.labels[key] = val; 117 | const sep = src.read(); 118 | if (sep === '}') break; 119 | if (sep !== ',') throw src.createParseRuntimeException(`expect ',' after label: ${key}=${val}`); 120 | } 121 | } 122 | src.skipChars(' \t'); 123 | ret.value = Number.parseFloat(src.readUntilTerminator(' \n\r')); 124 | if (src.isEof()) 125 | return ret; 126 | if (src.peek() === ' ') 127 | ret.timestamp = Number.parseFloat(src.readUntilTerminator('\n\r')); 128 | return ret; 129 | } 130 | 131 | updateCurrentMetric(metricLine: MetricLine) { 132 | if (metricLine.name === this.currentMetricKey) { 133 | const quantile = metricLine.labels.quantile; 134 | if (quantile) { 135 | delete metricLine.labels.quantile; 136 | this.currentMetric.getOrCreateValue(metricLine.labels).addQuantile(quantile, metricLine.value); 137 | return; 138 | } 139 | this.currentMetric.getOrCreateValue(metricLine.labels).value = metricLine.value; 140 | return; 141 | } 142 | 143 | if (metricLine.name === this.currentMetricKey + '_count') { 144 | this.currentMetric.getOrCreateValue(metricLine.labels).count = metricLine.value; 145 | return; 146 | } 147 | 148 | if (metricLine.name === this.currentMetricKey + '_sum') { 149 | this.currentMetric.getOrCreateValue(metricLine.labels).sum = metricLine.value; 150 | return; 151 | } 152 | 153 | if (metricLine.name === this.currentMetricKey + '_bucket') { 154 | const bucket = metricLine.labels.le; 155 | if (!bucket) 156 | throw new Error(`missing bucket label: ${JSON.stringify(metricLine)}`); 157 | delete metricLine.labels.le; 158 | this.currentMetric.getOrCreateValue(metricLine.labels).addBucket(bucket, metricLine.value); 159 | return; 160 | } 161 | 162 | // No matching, will assume it's an `count` without a type and help definition 163 | const metric = this.getOrCreateMetric(metricLine.name); 164 | metric.type = 'count'; 165 | metric.getOrCreateValue({}).value = metricLine.value; 166 | } 167 | 168 | getOrCreateMetric(name: string): Metric { 169 | let ret = this.result[name]; 170 | if (ret) 171 | return ret; 172 | ret = new Metric(); 173 | this.result[name] = ret; 174 | this.currentMetric = ret; 175 | this.currentMetricKey = name; 176 | return ret; 177 | } 178 | } -------------------------------------------------------------------------------- /src/models/TreeState.ts: -------------------------------------------------------------------------------- 1 | import { TDNode, TreeDoc, Bookmark, TDObjectCoder, TDNodeType, JSONPointer, LangUtil } from 'treedoc'; 2 | import History from './History'; 3 | import { ParserPlugin, ParseStatus } from './TDVOption'; 4 | import JSONParserPlugin from '../parsers/JSONParserPlugin'; 5 | import { Query, Column } from '../components/Vue2DataTable'; 6 | import TDVOption from './TDVOption'; 7 | 8 | const { doIfNotNull } = LangUtil; 9 | 10 | export interface Selection { 11 | start?: Bookmark; 12 | end?: Bookmark; 13 | } 14 | 15 | /** State that will be saved in history */ 16 | class CurState { 17 | selection: Selection = {}; 18 | constructor(public selected: TDNode | null = null) { } 19 | } 20 | 21 | export class TableNodeState { 22 | constructor( 23 | public query: Query, 24 | public expandedLevel: number, 25 | public columns: Column[], 26 | public isColumnExpanded: boolean) { } 27 | } 28 | 29 | export default class TreeState { 30 | parseResult = 'OBJECT'; 31 | history = new History(); 32 | initialNode?: TDNode | null; 33 | tableStateCache: Map = new Map(); 34 | 35 | tree: TreeDoc; 36 | 37 | // TODO: move to TableState 38 | hasTreeInTable = false; // If there's tree widget in the cells 39 | 40 | curState = new CurState(); 41 | 42 | maxPane = ''; 43 | curPan = ''; 44 | textWrap = false; 45 | showSource = [true]; 46 | showTree = [true]; 47 | showTable = [true]; 48 | codeView = [true]; 49 | 50 | constructor(treeData: TDNode | string | any, 51 | public parserPlugin: ParserPlugin = new JSONParserPlugin(), 52 | rootLabel = 'root', 53 | selectedPath: string[] = []) { 54 | this.tree = this.buildTree(treeData, rootLabel)!; 55 | if (this.tree) { 56 | this.tree.root.key = rootLabel; 57 | this.tree.root.freeze(); 58 | this.select(selectedPath, true); 59 | } 60 | } 61 | 62 | setCurPan(pan: string) { 63 | this.curPan = pan; 64 | console.log('setCurPan', pan); 65 | } 66 | 67 | setInitSOpt(opt?: TDVOption) { 68 | doIfNotNull(opt?.maxPane, $ => this.maxPane = $); 69 | doIfNotNull(opt?.textWrap, $ => this.textWrap = $); 70 | doIfNotNull(opt?.showSource, $ => this.showSource[0] = $); 71 | doIfNotNull(opt?.showTree, $ => this.showTree[0] = $); 72 | doIfNotNull(opt?.showTable, $ => this.showTable[0] = $); 73 | return this; 74 | } 75 | 76 | retainState(orgState: TreeState) { 77 | if (orgState == null) 78 | return this; 79 | this.maxPane = orgState.maxPane; 80 | this.textWrap = orgState.textWrap; 81 | this.showSource = orgState.showSource; 82 | this.showTree = orgState.showTree 83 | this.showTable = orgState.showTable; 84 | this.codeView = orgState.codeView; 85 | return this; 86 | } 87 | 88 | buildTree(treeData: TDNode | string | any, rootLabel: string) { 89 | if (!treeData || treeData.constructor.name === 'TDNode') { 90 | this.parseResult = 'TreeDoc'; 91 | return (treeData as TDNode).doc; 92 | } 93 | const tdNode = typeof(treeData) === 'string' ? this.parse(treeData) : TDObjectCoder.get().encode(treeData); 94 | return tdNode && tdNode.doc; 95 | } 96 | 97 | public select(node: TDNode | string | string[], initial = false): void { 98 | if (this.tree == null) 99 | return; 100 | 101 | let selectedNode: TDNode | null = null; 102 | if (!(node instanceof TDNode)) { 103 | // when initial, we specify noNull, for the case that current node name is edited, so it can't be selected 104 | // we will fullback to its parent. 105 | selectedNode = this.findNodeByPath(node, initial); 106 | } else 107 | selectedNode = node; 108 | 109 | if (initial) 110 | this.initialNode = selectedNode; 111 | if (this.curState.selected === selectedNode) 112 | return; 113 | this.curState = new CurState(selectedNode); 114 | this.curState.selected = selectedNode; 115 | if (selectedNode) 116 | this.history.append(this.curState); 117 | 118 | // We don't auto select in case it's initial. If auto selected, when user edit the source 119 | // the user won't be able to continuous editing. 120 | if (!initial) 121 | this.curState.selection = this.curState.selected!; 122 | } 123 | 124 | public saveTableState(node: TDNode, state: TableNodeState) { 125 | this.tableStateCache.set(node.pathAsString, state); 126 | } 127 | 128 | public getTableState(node: TDNode) { 129 | return this.tableStateCache.get(node.pathAsString); 130 | } 131 | 132 | get selected() { return this.curState.selected; } 133 | get selection() { return this.curState.selection; } 134 | 135 | public findNodeByPath(path: string | string[], noNull = false): TDNode { 136 | const cNode: TDNode = this.curState.selected || this.tree.root; 137 | let node = cNode.getByPath(path); 138 | if (node) 139 | return node; 140 | 141 | if (typeof(path) !== 'string') 142 | return cNode; 143 | 144 | // special handling for google API schema, e.g. https://www.googleapis.com/discovery/v1/apis/vision/v1p1beta1/rest 145 | node = cNode.getByPath('/schemas/' + path); 146 | if (node) 147 | return node; 148 | 149 | // using json pointer standard 150 | const jsonPointer = JSONPointer.get().parse(path); 151 | if (jsonPointer.docPath) { 152 | console.warn(`Cross document reference is not supported: ${path}`); 153 | return cNode; 154 | } 155 | node = cNode.getByPath(jsonPointer); 156 | if (node) 157 | return node; 158 | return cNode; 159 | } 160 | 161 | isRootSelected() { return this.tree != null && this.selected === this.tree.root; } 162 | isInitialNodeSelected() { return this.tree != null && this.selected === this.initialNode; } 163 | canBack() { return this.history.canBack(); } 164 | canForward() { return this.history.canForward(); } 165 | back() { this.curState = this.history.back()!; } 166 | forward() { this.curState = this.history.forward()!; } 167 | 168 | parse(jsonStr: string) { 169 | const result = this.parserPlugin.parse(jsonStr); 170 | this.parseResult = result.message; 171 | return result.result; 172 | } 173 | 174 | toggleMaxPane(pane: string) { 175 | if (this.maxPane) 176 | this.maxPane = ''; 177 | else 178 | this.maxPane = pane; 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/components/th-Filter.vue: -------------------------------------------------------------------------------- 1 | 59 | 108 | 153 | -------------------------------------------------------------------------------- /src/components/TreeViewItem.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 168 | 216 | -------------------------------------------------------------------------------- /src/sampleData.ts: -------------------------------------------------------------------------------- 1 | const jsonStr = ` 2 | { 3 | refundAmtMoney:"USD 15.32", 4 | activityHistory:[ 5 | { 6 | $type:"ActivityHist", 7 | $id:1234, 8 | creationDate:"2014/10/02 10:20:37", 9 | lastModifiedDate:"2014/10/02 10:20:37", 10 | timeStamp: 1599461650448, 11 | runtimeContext:"t=118", 12 | partitionKey:0, 13 | activityType:"1-buyerCreateCancel", 14 | log:"http://www.google.com", 15 | }, 16 | { 17 | $type:"ActivityHistBoImpl", 18 | creationDate:"2014/10/02 11:15:13", 19 | lastModifiedDate:"2014/10/02 11:15:13", 20 | timeStamp: 1599481650448, 21 | runtimeContext:"m=t=148\nline2", 22 | partitionKey:0, 23 | activityType:"6-sellerApprove", 24 | }], 25 | current: { 26 | $ref: '#/activityHistory/1', 27 | }, 28 | first: { 29 | $ref: '#1234', 30 | }, 31 | }`; 32 | 33 | const yamlStr = ` 34 | - martin: 35 | name: Martin D'vloper 36 | job: Developer 37 | skills: 38 | - python 39 | - perl 40 | - pascal 41 | - tabitha: 42 | name: Tabitha Bitumen 43 | job: Developer 44 | skills: 45 | - lisp 46 | - fortran 47 | - erlang 48 | `; 49 | 50 | const yamlMultiDocStr = 51 | `document: 1 52 | name: 'John' 53 | --- 54 | document: 2 55 | name: 'config' 56 | `; 57 | 58 | const xmlStr = ` 59 | 61 | 4.0.0 62 | com.jsonex 63 | jcParent 64 | 0.0.1-SNAPSHOT 65 | pom 66 | JSONCoder Parent 67 | JSONCoder Parent 68 | https://github.com/eBay/jsonex.git 69 | 70 | 71 | 72 | jianwu 73 | Jianwu Chen 74 | jianchen@ebay.com 75 | eBay 76 | http://www.ebay.com 77 | 78 | architect 79 | developer 80 | 81 | America/San_Francisco 82 | 83 | 84 | 85 | 86 | 87 | 88 | \${project.groupId} 89 | core 90 | \${project.version} 91 | 92 | 93 | org.projectlombok 94 | lombok 95 | 1.18.8 96 | provided 97 | 98 | 99 | junit 100 | junit 101 | test 102 | 4.8.1 103 | 104 | 105 | 106 | 107 | `; 108 | 109 | const fxml = ` 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | `; 122 | 123 | const csvStr = ` 124 | field1,field2,field3 125 | v11,v12,v13 126 | v21, "v2l1 127 | V2l2" ,v23 128 | "v31""v31","v32""""v32",v33 129 | `; 130 | 131 | const mapToStringStr = '{K1=v1, k2=123, k3={c=Test with ,in}, k4=[ab,c, def]}'; 132 | const lombokToString = "TestBean(treeMap={key1=value1}, linkedList1=[value1], intField=100, floatField=1.4, dateField=Wed Dec 31 19:23:32 PST 1969, bean2=TestBean2(strField=it's a string value, enumField=value2))" 133 | 134 | export const prometheusStr = String.raw` 135 | # Sample from: https://prometheus.io/docs/instrumenting/exposition_formats/#text-based-format 136 | # HELP http_requests_total The total number of HTTP requests. 137 | # TYPE http_requests_total counter 138 | http_requests_total{method="post",code="200"} 1027 1395066363000 139 | http_requests_total{method="post",code="400"} 3 1395066363000 140 | 141 | # Escaping in label values: 142 | msdos_file_access_time_seconds{path="C:\\DIR\\FILE.TXT",error="Cannot find file:\n\"FILE.TXT\""} 1.458255915e9 143 | 144 | # Minimalistic line: 145 | metric_without_timestamp_and_labels 12.47 146 | 147 | # Minimalistic line: 148 | # HELP metric_without_timestamp_and_labels The total number of HTTP requests. 149 | # TYPE metric_without_timestamp_and_labels counter 150 | metric_without_timestamp_and_labels{} 12.47 151 | 152 | # A weird metric from before the epoch: 153 | something_weird{problem="division by zero"} +Inf -3982045 154 | 155 | # A histogram, which has a pretty complex representation in the text format: 156 | # HELP http_request_duration_seconds A histogram of the request duration. 157 | # TYPE http_request_duration_seconds histogram 158 | http_request_duration_seconds_bucket{le="0.05"} 24054 159 | http_request_duration_seconds_bucket{le="0.1"} 33444 160 | http_request_duration_seconds_bucket{le="0.2"} 100392 161 | http_request_duration_seconds_bucket{le="0.5"} 129389 162 | http_request_duration_seconds_bucket{le="1"} 133988 163 | http_request_duration_seconds_bucket{le="+Inf"} 144320 164 | http_request_duration_seconds_sum 53423 165 | http_request_duration_seconds_count 144320 166 | 167 | # HELP thanos_objstore_bucket_operation_duration_seconds Duration of successful operations against the bucket 168 | # TYPE thanos_objstore_bucket_operation_duration_seconds histogram 169 | thanos_objstore_bucket_operation_duration_seconds_bucket{bucket="thanos",operation="attributes",le="0.01"} 0 170 | thanos_objstore_bucket_operation_duration_seconds_bucket{bucket="thanos",operation="attributes",le="0.1"} 0 171 | 172 | # HELP go_gc_duration_seconds A summary of the pause duration of garbage collection cycles. 173 | # TYPE go_gc_duration_seconds summary 174 | go_gc_duration_seconds{quantile="0"} 0 175 | go_gc_duration_seconds{quantile="0.25"} 0 176 | go_gc_duration_seconds{quantile="0.5"} 0 177 | go_gc_duration_seconds{quantile="0.75"} 0 178 | go_gc_duration_seconds{quantile="1"} 0 179 | go_gc_duration_seconds_sum 0 180 | go_gc_duration_seconds_count 0 181 | 182 | # Finally a summary, which has a complex representation, too: 183 | # HELP rpc_duration_seconds A summary of the RPC duration in seconds. 184 | # TYPE rpc_duration_seconds summary 185 | rpc_duration_seconds{quantile="0.01"} 3102 186 | rpc_duration_seconds{quantile="0.05"} 3272 187 | rpc_duration_seconds{quantile="0.5"} 4773 188 | rpc_duration_seconds{quantile="0.9"} 9001 189 | rpc_duration_seconds{quantile="0.99"} 76656 190 | rpc_duration_seconds_sum 1.7560473e+07 191 | rpc_duration_seconds_count 2693 192 | `; 193 | 194 | export default { 195 | jsonStr, 196 | yamlStr, 197 | xmlStr, 198 | csvStr, 199 | mapToStringStr, 200 | prometheusStr, 201 | data: [ 202 | {text: 'empty', value: {}}, 203 | { 204 | text: 'jsonStr', 205 | value: jsonStr, 206 | }, 207 | { 208 | text: 'object', 209 | value: { 210 | testArray: ['Just a Test String', 'in a Test Array', 0, 1, true, false], 211 | component: 'vue-json-tree-view', 212 | descripton: 'A JSON Tree View built in Vue.js', 213 | tags: [{ name: 'vue.js' }, { name: 'JSON' }], 214 | steps: [ 215 | 'HTML Template', 216 | 'Root Component', 217 | 'View Component', 218 | { 219 | 'Transformation Logic': ['Transform Objects', 'Transform Arrays', 'Transform Values'], 220 | }, 221 | 'Animate', 222 | 'Allow Options', 223 | 'Blog about it...', 224 | ], 225 | obj: { key1: 'val1', key2: 'val2' }, 226 | }, 227 | }, 228 | { 229 | text: 'array', 230 | value: [ 231 | { 232 | col1: 'value11', 233 | col2: 'value12', 234 | }, 235 | { 236 | col1: 'value21', 237 | col3: 'value23', 238 | }, 239 | 'value', 240 | { 241 | col1: 'value31', 242 | col2: 'value32', 243 | col3: 'value33', 244 | }, 245 | [ 246 | 'abc', 247 | 'def', 248 | { a: 1, b: 2 }, 249 | ], 250 | ], 251 | }, 252 | { 253 | text: 'textproto', 254 | value: 255 | `n: { 256 | n1: { 257 | n11: 1 258 | # Duplicated key; ':' is emitted before '{' 259 | n11 { 260 | n111: false 261 | } 262 | n12: "2" 263 | } 264 | # Multi-line comments 265 | # Line2 266 | ######## 267 | n1: { 268 | n11: "abcd" 269 | # Extension keys 270 | [d.e.f]: 4 271 | n11: "multiline 1\n" 272 | 'line2' 273 | } 274 | escapeStr: "a\\tb\\nc\\vd\\u0020e\\vf", 275 | htmlStr: "\nThis is a link\n" 276 | n2: [1,2,3] 277 | n2 [3,4,5] # ':' is emitted before '[' 278 | "n3" [6, 7, 8, 9] 279 | }`, 280 | }, 281 | { 282 | text: 'json5', 283 | value: 284 | `// https://spec.json5.org/ 285 | { 286 | // comments 287 | unquoted: 'and you can quote me on that', 288 | singleQuotes: 'I can use "double quotes" here', 289 | lineBreaks: "Look, Mom! \ 290 | No \\n's!", 291 | hexadecimal: 0xdecaf, 292 | leadingDecimalPoint: .8675309, andTrailing: 8675309., 293 | positiveSign: +1, 294 | trailingComma: 'in objects', andIn: ['arrays',], 295 | "backwardsCompatible": "with JSON", 296 | } 297 | `, 298 | }, 299 | { 300 | text: 'jsonex', 301 | value: 302 | `// Some comments 303 | { 304 | "total": 100000000000000000000, 305 | "longNum": 10000000000, 306 | "limit": 10, 307 | /* block comments */ 308 | "data": [ 309 | { 310 | "name": "Some Name 1", // More line comments 311 | "address": { 312 | "streetLine": "1st st", 313 | city: "san jose", 314 | }, 315 | "createdAt": "2017-07-14T17:17:33.010Z", 316 | }, 317 | { 318 | "name": "Some Name 2", 319 | "address": /*comments*/ { 320 | "streetLine": "2nd st", 321 | city: "san jose", 322 | }, 323 | "createdAt": "2017-07-14T17:17:33.010Z", 324 | }, 325 | \`Multiple line literal 326 | Line2\` 327 | ], 328 | } 329 | `, 330 | }, 331 | { 332 | text: 'hjson', 333 | value: 334 | `{ 335 | // use #, // or /**/ comments, 336 | // omit quotes for keys 337 | key: 1 338 | // omit quotes for strings 339 | contains: everything on this line 340 | // omit commas at the end of a line 341 | cool: { 342 | foo: 1 343 | bar: 2 344 | } 345 | // allow trailing commas 346 | list: [ 347 | 1, 348 | 2, 349 | ] 350 | // and use multiline strings 351 | realist: 352 | ''' 353 | My half empty glass, 354 | I will fill your empty half. 355 | Now you are half full. 356 | ''' 357 | } 358 | `, 359 | }, 360 | { 361 | text: 'yaml', 362 | value: yamlStr, 363 | }, 364 | { 365 | text: 'yaml-Multi-Doc', 366 | value: yamlMultiDocStr, 367 | }, 368 | { 369 | text: 'xml', 370 | value: xmlStr, 371 | }, 372 | { 373 | text: 'fxml (JavaFx)', 374 | value: fxml, 375 | }, 376 | { 377 | text: 'csv', 378 | value: csvStr, 379 | }, 380 | { 381 | text: 'map.toString', 382 | value: mapToStringStr, 383 | }, 384 | { 385 | text: 'lombok.toString', 386 | value: lombokToString, 387 | }, 388 | 389 | { 390 | text: 'prometheus', 391 | value: prometheusStr, 392 | }, 393 | ] as {text: string, value: any}[], 394 | }; 395 | -------------------------------------------------------------------------------- /public/privacy_policy_chmreaderx.html: -------------------------------------------------------------------------------- 1 |

Privacy Policy

2 |

Last updated: July 01, 2024

3 |

This Privacy Policy describes Our policies and procedures on the collection, use and disclosure of Your information when You use the Service and tells You about Your privacy rights and how the law protects You.

4 |

We use Your Personal data to provide and improve the Service. By using the Service, You agree to the collection and use of information in accordance with this Privacy Policy. This Privacy Policy has been created with the help of the Free Privacy Policy Generator.

5 |

Interpretation and Definitions

6 |

Interpretation

7 |

The words of which the initial letter is capitalized have meanings defined under the following conditions. The following definitions shall have the same meaning regardless of whether they appear in singular or in plural.

8 |

Definitions

9 |

For the purposes of this Privacy Policy:

10 |
    11 |
  • 12 |

    Account means a unique account created for You to access our Service or parts of our Service.

    13 |
  • 14 |
  • 15 |

    Affiliate means an entity that controls, is controlled by or is under common control with a party, where "control" means ownership of 50% or more of the shares, equity interest or other securities entitled to vote for election of directors or other managing authority.

    16 |
  • 17 |
  • 18 |

    Application refers to ChmReaderX, the software program provided by the Company.

    19 |
  • 20 |
  • 21 |

    Company (referred to as either "the Company", "We", "Us" or "Our" in this Agreement) refers to ChmReaderX.

    22 |
  • 23 |
  • 24 |

    Country refers to: California, United States

    25 |
  • 26 |
  • 27 |

    Device means any device that can access the Service such as a computer, a cellphone or a digital tablet.

    28 |
  • 29 |
  • 30 |

    Personal Data is any information that relates to an identified or identifiable individual.

    31 |
  • 32 |
  • 33 |

    Service refers to the Application.

    34 |
  • 35 |
  • 36 |

    Service Provider means any natural or legal person who processes the data on behalf of the Company. It refers to third-party companies or individuals employed by the Company to facilitate the Service, to provide the Service on behalf of the Company, to perform services related to the Service or to assist the Company in analyzing how the Service is used.

    37 |
  • 38 |
  • 39 |

    Usage Data refers to data collected automatically, either generated by the use of the Service or from the Service infrastructure itself (for example, the duration of a page visit).

    40 |
  • 41 |
  • 42 |

    You means the individual accessing or using the Service, or the company, or other legal entity on behalf of which such individual is accessing or using the Service, as applicable.

    43 |
  • 44 |
45 |

Collecting and Using Your Personal Data

46 |

Types of Data Collected

47 |

Personal Data

48 |

While using Our Service, We may ask You to provide Us with certain personally identifiable information that can be used to contact or identify You. Personally identifiable information may include, but is not limited to:

49 |
    50 |
  • Usage Data
  • 51 |
52 |

Usage Data

53 |

Usage Data is collected automatically when using the Service.

54 |

Usage Data may include information such as Your Device's Internet Protocol address (e.g. IP address), browser type, browser version, the pages of our Service that You visit, the time and date of Your visit, the time spent on those pages, unique device identifiers and other diagnostic data.

55 |

When You access the Service by or through a mobile device, We may collect certain information automatically, including, but not limited to, the type of mobile device You use, Your mobile device unique ID, the IP address of Your mobile device, Your mobile operating system, the type of mobile Internet browser You use, unique device identifiers and other diagnostic data.

56 |

We may also collect information that Your browser sends whenever You visit our Service or when You access the Service by or through a mobile device.

57 |

Use of Your Personal Data

58 |

The Company may use Personal Data for the following purposes:

59 |
    60 |
  • 61 |

    To provide and maintain our Service, including to monitor the usage of our Service.

    62 |
  • 63 |
  • 64 |

    To manage Your Account: to manage Your registration as a user of the Service. The Personal Data You provide can give You access to different functionalities of the Service that are available to You as a registered user.

    65 |
  • 66 |
  • 67 |

    For the performance of a contract: the development, compliance and undertaking of the purchase contract for the products, items or services You have purchased or of any other contract with Us through the Service.

    68 |
  • 69 |
  • 70 |

    To contact You: To contact You by email, telephone calls, SMS, or other equivalent forms of electronic communication, such as a mobile application's push notifications regarding updates or informative communications related to the functionalities, products or contracted services, including the security updates, when necessary or reasonable for their implementation.

    71 |
  • 72 |
  • 73 |

    To provide You with news, special offers and general information about other goods, services and events which we offer that are similar to those that you have already purchased or enquired about unless You have opted not to receive such information.

    74 |
  • 75 |
  • 76 |

    To manage Your requests: To attend and manage Your requests to Us.

    77 |
  • 78 |
  • 79 |

    For business transfers: We may use Your information to evaluate or conduct a merger, divestiture, restructuring, reorganization, dissolution, or other sale or transfer of some or all of Our assets, whether as a going concern or as part of bankruptcy, liquidation, or similar proceeding, in which Personal Data held by Us about our Service users is among the assets transferred.

    80 |
  • 81 |
  • 82 |

    For other purposes: We may use Your information for other purposes, such as data analysis, identifying usage trends, determining the effectiveness of our promotional campaigns and to evaluate and improve our Service, products, services, marketing and your experience.

    83 |
  • 84 |
85 |

We may share Your personal information in the following situations:

86 |
    87 |
  • With Service Providers: We may share Your personal information with Service Providers to monitor and analyze the use of our Service, to contact You.
  • 88 |
  • For business transfers: We may share or transfer Your personal information in connection with, or during negotiations of, any merger, sale of Company assets, financing, or acquisition of all or a portion of Our business to another company.
  • 89 |
  • With Affiliates: We may share Your information with Our affiliates, in which case we will require those affiliates to honor this Privacy Policy. Affiliates include Our parent company and any other subsidiaries, joint venture partners or other companies that We control or that are under common control with Us.
  • 90 |
  • With business partners: We may share Your information with Our business partners to offer You certain products, services or promotions.
  • 91 |
  • With other users: when You share personal information or otherwise interact in the public areas with other users, such information may be viewed by all users and may be publicly distributed outside.
  • 92 |
  • With Your consent: We may disclose Your personal information for any other purpose with Your consent.
  • 93 |
94 |

Retention of Your Personal Data

95 |

The Company will retain Your Personal Data only for as long as is necessary for the purposes set out in this Privacy Policy. We will retain and use Your Personal Data to the extent necessary to comply with our legal obligations (for example, if we are required to retain your data to comply with applicable laws), resolve disputes, and enforce our legal agreements and policies.

96 |

The Company will also retain Usage Data for internal analysis purposes. Usage Data is generally retained for a shorter period of time, except when this data is used to strengthen the security or to improve the functionality of Our Service, or We are legally obligated to retain this data for longer time periods.

97 |

Transfer of Your Personal Data

98 |

Your information, including Personal Data, is processed at the Company's operating offices and in any other places where the parties involved in the processing are located. It means that this information may be transferred to — and maintained on — computers located outside of Your state, province, country or other governmental jurisdiction where the data protection laws may differ than those from Your jurisdiction.

99 |

Your consent to this Privacy Policy followed by Your submission of such information represents Your agreement to that transfer.

100 |

The Company will take all steps reasonably necessary to ensure that Your data is treated securely and in accordance with this Privacy Policy and no transfer of Your Personal Data will take place to an organization or a country unless there are adequate controls in place including the security of Your data and other personal information.

101 |

Delete Your Personal Data

102 |

You have the right to delete or request that We assist in deleting the Personal Data that We have collected about You.

103 |

Our Service may give You the ability to delete certain information about You from within the Service.

104 |

You may update, amend, or delete Your information at any time by signing in to Your Account, if you have one, and visiting the account settings section that allows you to manage Your personal information. You may also contact Us to request access to, correct, or delete any personal information that You have provided to Us.

105 |

Please note, however, that We may need to retain certain information when we have a legal obligation or lawful basis to do so.

106 |

Disclosure of Your Personal Data

107 |

Business Transactions

108 |

If the Company is involved in a merger, acquisition or asset sale, Your Personal Data may be transferred. We will provide notice before Your Personal Data is transferred and becomes subject to a different Privacy Policy.

109 |

Law enforcement

110 |

Under certain circumstances, the Company may be required to disclose Your Personal Data if required to do so by law or in response to valid requests by public authorities (e.g. a court or a government agency).

111 |

Other legal requirements

112 |

The Company may disclose Your Personal Data in the good faith belief that such action is necessary to:

113 |
    114 |
  • Comply with a legal obligation
  • 115 |
  • Protect and defend the rights or property of the Company
  • 116 |
  • Prevent or investigate possible wrongdoing in connection with the Service
  • 117 |
  • Protect the personal safety of Users of the Service or the public
  • 118 |
  • Protect against legal liability
  • 119 |
120 |

Security of Your Personal Data

121 |

The security of Your Personal Data is important to Us, but remember that no method of transmission over the Internet, or method of electronic storage is 100% secure. While We strive to use commercially acceptable means to protect Your Personal Data, We cannot guarantee its absolute security.

122 |

Children's Privacy

123 |

Our Service does not address anyone under the age of 13. We do not knowingly collect personally identifiable information from anyone under the age of 13. If You are a parent or guardian and You are aware that Your child has provided Us with Personal Data, please contact Us. If We become aware that We have collected Personal Data from anyone under the age of 13 without verification of parental consent, We take steps to remove that information from Our servers.

124 |

If We need to rely on consent as a legal basis for processing Your information and Your country requires consent from a parent, We may require Your parent's consent before We collect and use that information.

125 |

Links to Other Websites

126 |

Our Service may contain links to other websites that are not operated by Us. If You click on a third party link, You will be directed to that third party's site. We strongly advise You to review the Privacy Policy of every site You visit.

127 |

We have no control over and assume no responsibility for the content, privacy policies or practices of any third party sites or services.

128 |

Changes to this Privacy Policy

129 |

We may update Our Privacy Policy from time to time. We will notify You of any changes by posting the new Privacy Policy on this page.

130 |

We will let You know via email and/or a prominent notice on Our Service, prior to the change becoming effective and update the "Last updated" date at the top of this Privacy Policy.

131 |

You are advised to review this Privacy Policy periodically for any changes. Changes to this Privacy Policy are effective when they are posted on this page.

132 |

Contact Us

133 |

If you have any questions about this Privacy Policy, You can contact us:

134 |
    135 |
  • By email: jianwu@gmail.com
  • 136 |
-------------------------------------------------------------------------------- /src/components/JsonTreeTable.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 390 | 391 | 457 | --------------------------------------------------------------------------------