source: trip-planner-front/node_modules/@discoveryjs/json-ext/dist/json-ext.js@ e29cc2e

Last change on this file since e29cc2e was 6a3a178, checked in by Ema <ema_spirova@…>, 3 years ago

initial commit

  • Property mode set to 100644
File size: 27.8 KB
Line 
1(function (global, factory) {
2 typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
3 typeof define === 'function' && define.amd ? define(factory) :
4 (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.jsonExt = factory());
5}(this, (function () { 'use strict';
6
7 var name = "@discoveryjs/json-ext";
8 var version = "0.5.3";
9 var description = "A set of utilities that extend the use of JSON";
10 var keywords = [
11 "json",
12 "utils",
13 "stream",
14 "async",
15 "promise",
16 "stringify",
17 "info"
18 ];
19 var author = "Roman Dvornov <rdvornov@gmail.com> (https://github.com/lahmatiy)";
20 var license = "MIT";
21 var repository = "discoveryjs/json-ext";
22 var main = "./src/index";
23 var browser = {
24 "./src/stringify-stream.js": "./src/stringify-stream-browser.js",
25 "./src/text-decoder.js": "./src/text-decoder-browser.js"
26 };
27 var scripts = {
28 test: "mocha --reporter progress",
29 lint: "eslint src test",
30 "lint-and-test": "npm run lint && npm test",
31 build: "rollup --config",
32 "test:all": "npm run test:src && npm run test:dist",
33 "test:src": "npm test",
34 "test:dist": "cross-env MODE=dist npm test && cross-env MODE=dist-min npm test",
35 "build-and-test": "npm run build && npm run test:dist",
36 coverage: "nyc npm test",
37 travis: "nyc npm run lint-and-test && npm run build-and-test && npm run coveralls",
38 coveralls: "nyc report --reporter=text-lcov | coveralls",
39 prepublishOnly: "npm run build"
40 };
41 var dependencies = {
42 };
43 var devDependencies = {
44 "@rollup/plugin-commonjs": "^15.1.0",
45 "@rollup/plugin-json": "^4.1.0",
46 "@rollup/plugin-node-resolve": "^9.0.0",
47 chalk: "^4.1.0",
48 coveralls: "^3.1.0",
49 "cross-env": "^7.0.3",
50 eslint: "^7.6.0",
51 mocha: "^8.1.1",
52 nyc: "^15.1.0",
53 rollup: "^2.28.2",
54 "rollup-plugin-terser": "^7.0.2"
55 };
56 var engines = {
57 node: ">=10.0.0"
58 };
59 var files = [
60 "dist",
61 "src"
62 ];
63 var require$$0 = {
64 name: name,
65 version: version,
66 description: description,
67 keywords: keywords,
68 author: author,
69 license: license,
70 repository: repository,
71 main: main,
72 browser: browser,
73 scripts: scripts,
74 dependencies: dependencies,
75 devDependencies: devDependencies,
76 engines: engines,
77 files: files
78 };
79
80 const PrimitiveType = 1;
81 const ObjectType = 2;
82 const ArrayType = 3;
83 const PromiseType = 4;
84 const ReadableStringType = 5;
85 const ReadableObjectType = 6;
86 // https://tc39.es/ecma262/#table-json-single-character-escapes
87 const escapableCharCodeSubstitution$1 = { // JSON Single Character Escape Sequences
88 0x08: '\\b',
89 0x09: '\\t',
90 0x0a: '\\n',
91 0x0c: '\\f',
92 0x0d: '\\r',
93 0x22: '\\\"',
94 0x5c: '\\\\'
95 };
96
97 function isLeadingSurrogate$1(code) {
98 return code >= 0xD800 && code <= 0xDBFF;
99 }
100
101 function isTrailingSurrogate$1(code) {
102 return code >= 0xDC00 && code <= 0xDFFF;
103 }
104
105 function isReadableStream$1(value) {
106 return (
107 typeof value.pipe === 'function' &&
108 typeof value._read === 'function' &&
109 typeof value._readableState === 'object' && value._readableState !== null
110 );
111 }
112
113 function replaceValue$1(holder, key, value, replacer) {
114 if (value && typeof value.toJSON === 'function') {
115 value = value.toJSON();
116 }
117
118 if (replacer !== null) {
119 value = replacer.call(holder, String(key), value);
120 }
121
122 switch (typeof value) {
123 case 'function':
124 case 'symbol':
125 value = undefined;
126 break;
127
128 case 'object':
129 if (value !== null) {
130 const cls = value.constructor;
131 if (cls === String || cls === Number || cls === Boolean) {
132 value = value.valueOf();
133 }
134 }
135 break;
136 }
137
138 return value;
139 }
140
141 function getTypeNative$1(value) {
142 if (value === null || typeof value !== 'object') {
143 return PrimitiveType;
144 }
145
146 if (Array.isArray(value)) {
147 return ArrayType;
148 }
149
150 return ObjectType;
151 }
152
153 function getTypeAsync$1(value) {
154 if (value === null || typeof value !== 'object') {
155 return PrimitiveType;
156 }
157
158 if (typeof value.then === 'function') {
159 return PromiseType;
160 }
161
162 if (isReadableStream$1(value)) {
163 return value._readableState.objectMode ? ReadableObjectType : ReadableStringType;
164 }
165
166 if (Array.isArray(value)) {
167 return ArrayType;
168 }
169
170 return ObjectType;
171 }
172
173 function normalizeReplacer$1(replacer) {
174 if (typeof replacer === 'function') {
175 return replacer;
176 }
177
178 if (Array.isArray(replacer)) {
179 const allowlist = new Set(replacer
180 .map(item => {
181 const cls = item && item.constructor;
182 return cls === String || cls === Number ? String(item) : null;
183 })
184 .filter(item => typeof item === 'string')
185 );
186
187 return [...allowlist];
188 }
189
190 return null;
191 }
192
193 function normalizeSpace$1(space) {
194 if (typeof space === 'number') {
195 if (!Number.isFinite(space) || space < 1) {
196 return false;
197 }
198
199 return ' '.repeat(Math.min(space, 10));
200 }
201
202 if (typeof space === 'string') {
203 return space.slice(0, 10) || false;
204 }
205
206 return false;
207 }
208
209 var utils = {
210 escapableCharCodeSubstitution: escapableCharCodeSubstitution$1,
211 isLeadingSurrogate: isLeadingSurrogate$1,
212 isTrailingSurrogate: isTrailingSurrogate$1,
213 type: {
214 PRIMITIVE: PrimitiveType,
215 PROMISE: PromiseType,
216 ARRAY: ArrayType,
217 OBJECT: ObjectType,
218 STRING_STREAM: ReadableStringType,
219 OBJECT_STREAM: ReadableObjectType
220 },
221
222 isReadableStream: isReadableStream$1,
223 replaceValue: replaceValue$1,
224 getTypeNative: getTypeNative$1,
225 getTypeAsync: getTypeAsync$1,
226 normalizeReplacer: normalizeReplacer$1,
227 normalizeSpace: normalizeSpace$1
228 };
229
230 const {
231 normalizeReplacer,
232 normalizeSpace,
233 replaceValue,
234 getTypeNative,
235 getTypeAsync,
236 isLeadingSurrogate,
237 isTrailingSurrogate,
238 escapableCharCodeSubstitution,
239 type: {
240 PRIMITIVE,
241 OBJECT,
242 ARRAY,
243 PROMISE,
244 STRING_STREAM,
245 OBJECT_STREAM
246 }
247 } = utils;
248 const charLength2048 = Array.from({ length: 2048 }).map((_, code) => {
249 if (escapableCharCodeSubstitution.hasOwnProperty(code)) {
250 return 2; // \X
251 }
252
253 if (code < 0x20) {
254 return 6; // \uXXXX
255 }
256
257 return code < 128 ? 1 : 2; // UTF8 bytes
258 });
259
260 function stringLength(str) {
261 let len = 0;
262 let prevLeadingSurrogate = false;
263
264 for (let i = 0; i < str.length; i++) {
265 const code = str.charCodeAt(i);
266
267 if (code < 2048) {
268 len += charLength2048[code];
269 } else if (isLeadingSurrogate(code)) {
270 len += 6; // \uXXXX since no pair with trailing surrogate yet
271 prevLeadingSurrogate = true;
272 continue;
273 } else if (isTrailingSurrogate(code)) {
274 len = prevLeadingSurrogate
275 ? len - 2 // surrogate pair (4 bytes), since we calculate prev leading surrogate as 6 bytes, substruct 2 bytes
276 : len + 6; // \uXXXX
277 } else {
278 len += 3; // code >= 2048 is 3 bytes length for UTF8
279 }
280
281 prevLeadingSurrogate = false;
282 }
283
284 return len + 2; // +2 for quotes
285 }
286
287 function primitiveLength(value) {
288 switch (typeof value) {
289 case 'string':
290 return stringLength(value);
291
292 case 'number':
293 return Number.isFinite(value) ? String(value).length : 4 /* null */;
294
295 case 'boolean':
296 return value ? 4 /* true */ : 5 /* false */;
297
298 case 'undefined':
299 case 'object':
300 return 4; /* null */
301
302 default:
303 return 0;
304 }
305 }
306
307 function spaceLength(space) {
308 space = normalizeSpace(space);
309 return typeof space === 'string' ? space.length : 0;
310 }
311
312 var stringifyInfo = function jsonStringifyInfo(value, replacer, space, options) {
313 function walk(holder, key, value) {
314 if (stop) {
315 return;
316 }
317
318 value = replaceValue(holder, key, value, replacer);
319
320 let type = getType(value);
321
322 // check for circular structure
323 if (type !== PRIMITIVE && stack.has(value)) {
324 circular.add(value);
325 length += 4; // treat as null
326
327 if (!options.continueOnCircular) {
328 stop = true;
329 }
330
331 return;
332 }
333
334 switch (type) {
335 case PRIMITIVE:
336 if (value !== undefined || Array.isArray(holder)) {
337 length += primitiveLength(value);
338 } else if (holder === root) {
339 length += 9; // FIXME: that's the length of undefined, should we normalize behaviour to convert it to null?
340 }
341 break;
342
343 case OBJECT: {
344 if (visited.has(value)) {
345 duplicate.add(value);
346 length += visited.get(value);
347 break;
348 }
349
350 const valueLength = length;
351 let entries = 0;
352
353 length += 2; // {}
354
355 stack.add(value);
356
357 for (const key in value) {
358 if (hasOwnProperty.call(value, key) && (allowlist === null || allowlist.has(key))) {
359 const prevLength = length;
360 walk(value, key, value[key]);
361
362 if (prevLength !== length) {
363 // value is printed
364 length += stringLength(key) + 1; // "key":
365 entries++;
366 }
367 }
368 }
369
370 if (entries > 1) {
371 length += entries - 1; // commas
372 }
373
374 stack.delete(value);
375
376 if (space > 0 && entries > 0) {
377 length += (1 + (stack.size + 1) * space + 1) * entries; // for each key-value: \n{space}
378 length += 1 + stack.size * space; // for }
379 }
380
381 visited.set(value, length - valueLength);
382
383 break;
384 }
385
386 case ARRAY: {
387 if (visited.has(value)) {
388 duplicate.add(value);
389 length += visited.get(value);
390 break;
391 }
392
393 const valueLength = length;
394
395 length += 2; // []
396
397 stack.add(value);
398
399 for (let i = 0; i < value.length; i++) {
400 walk(value, i, value[i]);
401 }
402
403 if (value.length > 1) {
404 length += value.length - 1; // commas
405 }
406
407 stack.delete(value);
408
409 if (space > 0 && value.length > 0) {
410 length += (1 + (stack.size + 1) * space) * value.length; // for each element: \n{space}
411 length += 1 + stack.size * space; // for ]
412 }
413
414 visited.set(value, length - valueLength);
415
416 break;
417 }
418
419 case PROMISE:
420 case STRING_STREAM:
421 async.add(value);
422 break;
423
424 case OBJECT_STREAM:
425 length += 2; // []
426 async.add(value);
427 break;
428 }
429 }
430
431 let allowlist = null;
432 replacer = normalizeReplacer(replacer);
433
434 if (Array.isArray(replacer)) {
435 allowlist = new Set(replacer);
436 replacer = null;
437 }
438
439 space = spaceLength(space);
440 options = options || {};
441
442 const visited = new Map();
443 const stack = new Set();
444 const duplicate = new Set();
445 const circular = new Set();
446 const async = new Set();
447 const getType = options.async ? getTypeAsync : getTypeNative;
448 const root = { '': value };
449 let stop = false;
450 let length = 0;
451
452 walk(root, '', value);
453
454 return {
455 minLength: isNaN(length) ? Infinity : length,
456 circular: [...circular],
457 duplicate: [...duplicate],
458 async: [...async]
459 };
460 };
461
462 var stringifyStreamBrowser = () => {
463 throw new Error('Method is not supported');
464 };
465
466 var textDecoderBrowser = TextDecoder;
467
468 const { isReadableStream } = utils;
469
470
471 const STACK_OBJECT = 1;
472 const STACK_ARRAY = 2;
473 const decoder = new textDecoderBrowser();
474
475 function isObject(value) {
476 return value !== null && typeof value === 'object';
477 }
478
479 function adjustPosition(error, parser) {
480 if (error.name === 'SyntaxError' && parser.jsonParseOffset) {
481 error.message = error.message.replace(/at position (\d+)/, (_, pos) =>
482 'at position ' + (Number(pos) + parser.jsonParseOffset)
483 );
484 }
485
486 return error;
487 }
488
489 function append(array, elements) {
490 // Note: Avoid to use array.push(...elements) since it may lead to
491 // "RangeError: Maximum call stack size exceeded" for a long arrays
492 const initialLength = array.length;
493 array.length += elements.length;
494
495 for (let i = 0; i < elements.length; i++) {
496 array[initialLength + i] = elements[i];
497 }
498 }
499
500 var parseChunked = function(chunkEmitter) {
501 let parser = new ChunkParser();
502
503 if (isObject(chunkEmitter) && isReadableStream(chunkEmitter)) {
504 return new Promise((resolve, reject) => {
505 chunkEmitter
506 .on('data', chunk => {
507 try {
508 parser.push(chunk);
509 } catch (e) {
510 reject(adjustPosition(e, parser));
511 parser = null;
512 }
513 })
514 .on('error', (e) => {
515 parser = null;
516 reject(e);
517 })
518 .on('end', () => {
519 try {
520 resolve(parser.finish());
521 } catch (e) {
522 reject(adjustPosition(e, parser));
523 } finally {
524 parser = null;
525 }
526 });
527 });
528 }
529
530 if (typeof chunkEmitter === 'function') {
531 const iterator = chunkEmitter();
532
533 if (isObject(iterator) && (Symbol.iterator in iterator || Symbol.asyncIterator in iterator)) {
534 return new Promise(async (resolve, reject) => {
535 try {
536 for await (const chunk of iterator) {
537 parser.push(chunk);
538 }
539
540 resolve(parser.finish());
541 } catch (e) {
542 reject(adjustPosition(e, parser));
543 } finally {
544 parser = null;
545 }
546 });
547 }
548 }
549
550 throw new Error(
551 'Chunk emitter should be readable stream, generator, ' +
552 'async generator or function returning an iterable object'
553 );
554 };
555
556 class ChunkParser {
557 constructor() {
558 this.value = undefined;
559 this.valueStack = null;
560
561 this.stack = new Array(100);
562 this.lastFlushDepth = 0;
563 this.flushDepth = 0;
564 this.stateString = false;
565 this.stateStringEscape = false;
566 this.pendingByteSeq = null;
567 this.pendingChunk = null;
568 this.chunkOffset = 0;
569 this.jsonParseOffset = 0;
570 }
571
572 parseAndAppend(fragment, wrap) {
573 // Append new entries or elements
574 if (this.stack[this.lastFlushDepth - 1] === STACK_OBJECT) {
575 if (wrap) {
576 this.jsonParseOffset--;
577 fragment = '{' + fragment + '}';
578 }
579
580 Object.assign(this.valueStack.value, JSON.parse(fragment));
581 } else {
582 if (wrap) {
583 this.jsonParseOffset--;
584 fragment = '[' + fragment + ']';
585 }
586
587 append(this.valueStack.value, JSON.parse(fragment));
588 }
589 }
590
591 prepareAddition(fragment) {
592 const { value } = this.valueStack;
593 const expectComma = Array.isArray(value)
594 ? value.length !== 0
595 : Object.keys(value).length !== 0;
596
597 if (expectComma) {
598 // Skip a comma at the beginning of fragment, otherwise it would
599 // fail to parse
600 if (fragment[0] === ',') {
601 this.jsonParseOffset++;
602 return fragment.slice(1);
603 }
604
605 // When value (an object or array) is not empty and a fragment
606 // doesn't start with a comma, a single valid fragment starting
607 // is a closing bracket. If it's not, a prefix is adding to fail
608 // parsing. Otherwise, the sequence of chunks can be successfully
609 // parsed, although it should not, e.g. ["[{}", "{}]"]
610 if (fragment[0] !== '}' && fragment[0] !== ']') {
611 this.jsonParseOffset -= 3;
612 return '[[]' + fragment;
613 }
614 }
615
616 return fragment;
617 }
618
619 flush(chunk, start, end) {
620 let fragment = chunk.slice(start, end);
621
622 // Save position correction an error in JSON.parse() if any
623 this.jsonParseOffset = this.chunkOffset + start;
624
625 // Prepend pending chunk if any
626 if (this.pendingChunk !== null) {
627 fragment = this.pendingChunk + fragment;
628 this.jsonParseOffset -= this.pendingChunk.length;
629 this.pendingChunk = null;
630 }
631
632 if (this.flushDepth === this.lastFlushDepth) {
633 // Depth didn't changed, so it's a root value or entry/element set
634 if (this.flushDepth > 0) {
635 this.parseAndAppend(this.prepareAddition(fragment), true);
636 } else {
637 // That's an entire value on a top level
638 this.value = JSON.parse(fragment);
639 this.valueStack = {
640 value: this.value,
641 prev: null
642 };
643 }
644 } else if (this.flushDepth > this.lastFlushDepth) {
645 // Add missed closing brackets/parentheses
646 for (let i = this.flushDepth - 1; i >= this.lastFlushDepth; i--) {
647 fragment += this.stack[i] === STACK_OBJECT ? '}' : ']';
648 }
649
650 if (this.lastFlushDepth === 0) {
651 // That's a root value
652 this.value = JSON.parse(fragment);
653 this.valueStack = {
654 value: this.value,
655 prev: null
656 };
657 } else {
658 this.parseAndAppend(this.prepareAddition(fragment), true);
659 }
660
661 // Move down to the depths to the last object/array, which is current now
662 for (let i = this.lastFlushDepth || 1; i < this.flushDepth; i++) {
663 let value = this.valueStack.value;
664
665 if (this.stack[i - 1] === STACK_OBJECT) {
666 // find last entry
667 let key;
668 // eslint-disable-next-line curly
669 for (key in value);
670 value = value[key];
671 } else {
672 // last element
673 value = value[value.length - 1];
674 }
675
676 this.valueStack = {
677 value,
678 prev: this.valueStack
679 };
680 }
681 } else /* this.flushDepth < this.lastFlushDepth */ {
682 fragment = this.prepareAddition(fragment);
683
684 // Add missed opening brackets/parentheses
685 for (let i = this.lastFlushDepth - 1; i >= this.flushDepth; i--) {
686 this.jsonParseOffset--;
687 fragment = (this.stack[i] === STACK_OBJECT ? '{' : '[') + fragment;
688 }
689
690 this.parseAndAppend(fragment, false);
691
692 for (let i = this.lastFlushDepth - 1; i >= this.flushDepth; i--) {
693 this.valueStack = this.valueStack.prev;
694 }
695 }
696
697 this.lastFlushDepth = this.flushDepth;
698 }
699
700 push(chunk) {
701 if (typeof chunk !== 'string') {
702 // Suppose chunk is Buffer or Uint8Array
703
704 // Prepend uncompleted byte sequence if any
705 if (this.pendingByteSeq !== null) {
706 const origRawChunk = chunk;
707 chunk = new Uint8Array(this.pendingByteSeq.length + origRawChunk.length);
708 chunk.set(this.pendingByteSeq);
709 chunk.set(origRawChunk, this.pendingByteSeq.length);
710 this.pendingByteSeq = null;
711 }
712
713 // In case Buffer/Uint8Array, an input is encoded in UTF8
714 // Seek for parts of uncompleted UTF8 symbol on the ending
715 // This makes sense only if we expect more chunks and last char is not multi-bytes
716 if (chunk[chunk.length - 1] > 127) {
717 for (let seqLength = 0; seqLength < chunk.length; seqLength++) {
718 const byte = chunk[chunk.length - 1 - seqLength];
719
720 // 10xxxxxx - 2nd, 3rd or 4th byte
721 // 110xxxxx – first byte of 2-byte sequence
722 // 1110xxxx - first byte of 3-byte sequence
723 // 11110xxx - first byte of 4-byte sequence
724 if (byte >> 6 === 3) {
725 seqLength++;
726
727 // If the sequence is really incomplete, then preserve it
728 // for the future chunk and cut off it from the current chunk
729 if ((seqLength !== 4 && byte >> 3 === 0b11110) ||
730 (seqLength !== 3 && byte >> 4 === 0b1110) ||
731 (seqLength !== 2 && byte >> 5 === 0b110)) {
732 this.pendingByteSeq = chunk.slice(chunk.length - seqLength);
733 chunk = chunk.slice(0, -seqLength);
734 }
735
736 break;
737 }
738 }
739 }
740
741 // Convert chunk to a string, since single decode per chunk
742 // is much effective than decode multiple small substrings
743 chunk = decoder.decode(chunk);
744 }
745
746 const chunkLength = chunk.length;
747 let lastFlushPoint = 0;
748 let flushPoint = 0;
749
750 // Main scan loop
751 scan: for (let i = 0; i < chunkLength; i++) {
752 if (this.stateString) {
753 for (; i < chunkLength; i++) {
754 if (this.stateStringEscape) {
755 this.stateStringEscape = false;
756 } else {
757 switch (chunk.charCodeAt(i)) {
758 case 0x22: /* " */
759 this.stateString = false;
760 continue scan;
761
762 case 0x5C: /* \ */
763 this.stateStringEscape = true;
764 }
765 }
766 }
767
768 break;
769 }
770
771 switch (chunk.charCodeAt(i)) {
772 case 0x22: /* " */
773 this.stateString = true;
774 this.stateStringEscape = false;
775 break;
776
777 case 0x2C: /* , */
778 flushPoint = i;
779 break;
780
781 case 0x7B: /* { */
782 // Open an object
783 flushPoint = i + 1;
784 this.stack[this.flushDepth++] = STACK_OBJECT;
785 break;
786
787 case 0x5B: /* [ */
788 // Open an array
789 flushPoint = i + 1;
790 this.stack[this.flushDepth++] = STACK_ARRAY;
791 break;
792
793 case 0x5D: /* ] */
794 case 0x7D: /* } */
795 // Close an object or array
796 flushPoint = i + 1;
797 this.flushDepth--;
798
799 if (this.flushDepth < this.lastFlushDepth) {
800 this.flush(chunk, lastFlushPoint, flushPoint);
801 lastFlushPoint = flushPoint;
802 }
803
804 break;
805
806 case 0x09: /* \t */
807 case 0x0A: /* \n */
808 case 0x0D: /* \r */
809 case 0x20: /* space */
810 // Move points forward when they points on current position and it's a whitespace
811 if (lastFlushPoint === i) {
812 lastFlushPoint++;
813 }
814
815 if (flushPoint === i) {
816 flushPoint++;
817 }
818
819 break;
820 }
821 }
822
823 if (flushPoint > lastFlushPoint) {
824 this.flush(chunk, lastFlushPoint, flushPoint);
825 }
826
827 // Produce pendingChunk if something left
828 if (flushPoint < chunkLength) {
829 if (this.pendingChunk !== null) {
830 // When there is already a pending chunk then no flush happened,
831 // appending entire chunk to pending one
832 this.pendingChunk += chunk;
833 } else {
834 // Create a pending chunk, it will start with non-whitespace since
835 // flushPoint was moved forward away from whitespaces on scan
836 this.pendingChunk = chunk.slice(flushPoint, chunkLength);
837 }
838 }
839
840 this.chunkOffset += chunkLength;
841 }
842
843 finish() {
844 if (this.pendingChunk !== null) {
845 this.flush('', 0, 0);
846 this.pendingChunk = null;
847 }
848
849 return this.value;
850 }
851 }
852
853 var src = {
854 version: require$$0.version,
855 stringifyInfo: stringifyInfo,
856 stringifyStream: stringifyStreamBrowser,
857 parseChunked: parseChunked
858 };
859
860 return src;
861
862})));
Note: See TracBrowser for help on using the repository browser.