From 3671fc067fc1246cd481e5df9eca3c58521aef71 Mon Sep 17 00:00:00 2001 From: Josef Date: Sat, 8 Mar 2025 09:16:54 +0100 Subject: [PATCH] Initial Commit --- browser/colors/Colors.ts | 182 ++++ browser/colors/HSLColor.ts | 410 ++++++++ browser/colors/HTMLColor.ts | 145 +++ browser/colors/HTMLColorNameTable.ts | 166 +++ browser/colors/HTMLColorParser.ts | 53 + browser/colors/RGBColor.ts | 221 ++++ browser/colors/YBColorSpace.ts | 29 + browser/dom/ClassFlag.ts | 232 +++++ browser/dom/DOMEditor.ts | 162 +++ browser/dom/DOMNameSpaces.ts | 5 + browser/dom/ElementAttribute.ts | 412 ++++++++ browser/dom/ElementType.ts | 172 ++++ browser/expressions/AsyncBooleanExpression.ts | 101 ++ browser/expressions/AsyncStringMatcher.ts | 76 ++ browser/expressions/BooleanExpression.ts | 114 +++ browser/expressions/StringMatcher.ts | 40 + browser/geometry/Range.ts | 115 +++ browser/graphs/ArrayChildrenTreeWalker.ts | 48 + browser/graphs/HTMLNodeTreeWalker.ts | 42 + browser/graphs/TreeWalker.ts | 266 +++++ browser/math/MathX.ts | 283 ++++++ browser/random/IncrementalIDGenerator.ts | 68 ++ browser/random/JSRandomEngine.ts | 14 + browser/random/LCG.ts | 73 ++ browser/random/RandomEngine.ts | 212 ++++ browser/random/RandomUIDGenerator.ts | 54 + browser/random/SeedGenerator.ts | 35 + browser/templates/ElementProcessor.ts | 7 + browser/templates/TemplateReplacer.ts | 406 ++++++++ browser/templates/TemplateSource.ts | 132 +++ browser/templates/TemplateSourceMatchers.ts | 118 +++ browser/templates/TemplatesManager.ts | 241 +++++ browser/templates/TemplatesSourceLexer.ts | 78 ++ .../styles-processor/StylesProcessor.ts | 11 + .../styles-processor/StylesProcessorLexer.ts | 168 ++++ browser/text/ExtendedRegex.ts | 96 ++ browser/text/Levehshtein.ts | 108 ++ browser/text/RegExpUtitlity.ts | 952 ++++++++++++++++++ browser/text/lexer/CLikeLexer.ts | 34 + browser/text/lexer/CSVLexer.ts | 28 + browser/text/lexer/HighlightedHTML.ts | 99 ++ browser/text/lexer/Lexer.ts | 170 ++++ browser/text/lexer/LexerEvent.ts | 80 ++ browser/text/lexer/LexerEventMatcher.ts | 94 ++ browser/text/lexer/LexerMatcher.ts | 85 ++ browser/text/lexer/LexerMatcherLibrary.ts | 95 ++ browser/text/lexer/LexerMatcherLibraryTest.ts | 91 ++ browser/text/lexer/LexerQuery.ts | 261 +++++ browser/text/lexer/LexerSequence.ts | 88 ++ browser/text/lexer/LexerType.ts | 143 +++ browser/text/lexer/PHPClasses.ts | 8 + browser/text/lexer/PHPLexer.ts | 77 ++ .../lexer/expressions/BinaryExpression.ts | 13 + .../text/lexer/expressions/ExpressionNode.ts | 15 + browser/text/lexer/expressions/LiteralNode.ts | 12 + browser/text/lexer/library/CSharpLanguage.ts | 13 + browser/text/lexer/library/GodotShader.ts | 21 + .../lexer/library/SteamworksNetLibrary.ts | 15 + browser/text/lexer/library/UnityLibrary.ts | 17 + .../text/lexer/parsing/OperatorResolver.ts | 4 + browser/text/lexer/parsing/Parser.ts | 73 ++ browser/text/lexer/parsing/ParserMessage.ts | 20 + browser/text/lexer/parsing/ParserPhase.ts | 21 + browser/text/lexer/parsing/PrecedenceLevel.ts | 7 + browser/text/lexer/parsing/SourceInfo.ts | 87 ++ browser/text/replacing/TextReplacement.ts | 21 + .../replacing/TextReplacementProcessor.ts | 24 + browser/text/replacing/TextReplacer.ts | 134 +++ browser/text/replacing/TextSelectionRange.ts | 99 ++ browser/text/replacing/TextSelector.ts | 128 +++ browser/text/replacing/VariableReplacer.ts | 16 + browser/tools/Arrays.ts | 179 ++++ browser/tsconfig.json | 20 + node/tsconfig.json | 7 + 74 files changed, 8346 insertions(+) create mode 100644 browser/colors/Colors.ts create mode 100644 browser/colors/HSLColor.ts create mode 100644 browser/colors/HTMLColor.ts create mode 100644 browser/colors/HTMLColorNameTable.ts create mode 100644 browser/colors/HTMLColorParser.ts create mode 100644 browser/colors/RGBColor.ts create mode 100644 browser/colors/YBColorSpace.ts create mode 100644 browser/dom/ClassFlag.ts create mode 100644 browser/dom/DOMEditor.ts create mode 100644 browser/dom/DOMNameSpaces.ts create mode 100644 browser/dom/ElementAttribute.ts create mode 100644 browser/dom/ElementType.ts create mode 100644 browser/expressions/AsyncBooleanExpression.ts create mode 100644 browser/expressions/AsyncStringMatcher.ts create mode 100644 browser/expressions/BooleanExpression.ts create mode 100644 browser/expressions/StringMatcher.ts create mode 100644 browser/geometry/Range.ts create mode 100644 browser/graphs/ArrayChildrenTreeWalker.ts create mode 100644 browser/graphs/HTMLNodeTreeWalker.ts create mode 100644 browser/graphs/TreeWalker.ts create mode 100644 browser/math/MathX.ts create mode 100644 browser/random/IncrementalIDGenerator.ts create mode 100644 browser/random/JSRandomEngine.ts create mode 100644 browser/random/LCG.ts create mode 100644 browser/random/RandomEngine.ts create mode 100644 browser/random/RandomUIDGenerator.ts create mode 100644 browser/random/SeedGenerator.ts create mode 100644 browser/templates/ElementProcessor.ts create mode 100644 browser/templates/TemplateReplacer.ts create mode 100644 browser/templates/TemplateSource.ts create mode 100644 browser/templates/TemplateSourceMatchers.ts create mode 100644 browser/templates/TemplatesManager.ts create mode 100644 browser/templates/TemplatesSourceLexer.ts create mode 100644 browser/templates/styles-processor/StylesProcessor.ts create mode 100644 browser/templates/styles-processor/StylesProcessorLexer.ts create mode 100644 browser/text/ExtendedRegex.ts create mode 100644 browser/text/Levehshtein.ts create mode 100644 browser/text/RegExpUtitlity.ts create mode 100644 browser/text/lexer/CLikeLexer.ts create mode 100644 browser/text/lexer/CSVLexer.ts create mode 100644 browser/text/lexer/HighlightedHTML.ts create mode 100644 browser/text/lexer/Lexer.ts create mode 100644 browser/text/lexer/LexerEvent.ts create mode 100644 browser/text/lexer/LexerEventMatcher.ts create mode 100644 browser/text/lexer/LexerMatcher.ts create mode 100644 browser/text/lexer/LexerMatcherLibrary.ts create mode 100644 browser/text/lexer/LexerMatcherLibraryTest.ts create mode 100644 browser/text/lexer/LexerQuery.ts create mode 100644 browser/text/lexer/LexerSequence.ts create mode 100644 browser/text/lexer/LexerType.ts create mode 100644 browser/text/lexer/PHPClasses.ts create mode 100644 browser/text/lexer/PHPLexer.ts create mode 100644 browser/text/lexer/expressions/BinaryExpression.ts create mode 100644 browser/text/lexer/expressions/ExpressionNode.ts create mode 100644 browser/text/lexer/expressions/LiteralNode.ts create mode 100644 browser/text/lexer/library/CSharpLanguage.ts create mode 100644 browser/text/lexer/library/GodotShader.ts create mode 100644 browser/text/lexer/library/SteamworksNetLibrary.ts create mode 100644 browser/text/lexer/library/UnityLibrary.ts create mode 100644 browser/text/lexer/parsing/OperatorResolver.ts create mode 100644 browser/text/lexer/parsing/Parser.ts create mode 100644 browser/text/lexer/parsing/ParserMessage.ts create mode 100644 browser/text/lexer/parsing/ParserPhase.ts create mode 100644 browser/text/lexer/parsing/PrecedenceLevel.ts create mode 100644 browser/text/lexer/parsing/SourceInfo.ts create mode 100644 browser/text/replacing/TextReplacement.ts create mode 100644 browser/text/replacing/TextReplacementProcessor.ts create mode 100644 browser/text/replacing/TextReplacer.ts create mode 100644 browser/text/replacing/TextSelectionRange.ts create mode 100644 browser/text/replacing/TextSelector.ts create mode 100644 browser/text/replacing/VariableReplacer.ts create mode 100644 browser/tools/Arrays.ts create mode 100644 browser/tsconfig.json create mode 100644 node/tsconfig.json diff --git a/browser/colors/Colors.ts b/browser/colors/Colors.ts new file mode 100644 index 0000000..54e325d --- /dev/null +++ b/browser/colors/Colors.ts @@ -0,0 +1,182 @@ +import { HTMLColorParser } from './HTMLColorParser'; + +export class Colors +{ + static lighten( color:string, value:number ) + { + let htmlColor = HTMLColorParser.fromString( color ); + + return htmlColor.lighten( value ) + ""; + } + + static saturate( color:string, value:number ) + { + let htmlColor = HTMLColorParser.fromString( color ); + + return htmlColor.saturate( value ) + ""; + } + + static shiftHue( color:string, value:number ) + { + let htmlColor = HTMLColorParser.fromString( color ); + + return htmlColor.shiftHue( value ) + ""; + } + + static fade( color:string, value:number ) + { + let htmlColor = HTMLColorParser.fromString( color ); + + return htmlColor.fade( value ) + ""; + } + + static readonly black = HTMLColorParser.fromStringHexColor( "#000000" ); + static readonly silver = HTMLColorParser.fromStringHexColor( "#c0c0c0" ); + static readonly gray = HTMLColorParser.fromStringHexColor( "#808080" ); + static readonly white = HTMLColorParser.fromStringHexColor( "#ffffff" ); + static readonly maroon = HTMLColorParser.fromStringHexColor( "#800000" ); + static readonly red = HTMLColorParser.fromStringHexColor( "#ff0000" ); + static readonly purple = HTMLColorParser.fromStringHexColor( "#800080" ); + static readonly fuchsia = HTMLColorParser.fromStringHexColor( "#ff00ff" ); + static readonly green = HTMLColorParser.fromStringHexColor( "#008000" ); + static readonly lime = HTMLColorParser.fromStringHexColor( "#00ff00" ); + static readonly olive = HTMLColorParser.fromStringHexColor( "#808000" ); + static readonly yellow = HTMLColorParser.fromStringHexColor( "#ffff00" ); + static readonly navy = HTMLColorParser.fromStringHexColor( "#000080" ); + static readonly blue = HTMLColorParser.fromStringHexColor( "#0000ff" ); + static readonly teal = HTMLColorParser.fromStringHexColor( "#008080" ); + static readonly aqua = HTMLColorParser.fromStringHexColor( "#00ffff" ); + static readonly orange = HTMLColorParser.fromStringHexColor( "#ffa500" ); + static readonly aliceblue = HTMLColorParser.fromStringHexColor( "#f0f8ff" ); + static readonly antiquewhite = HTMLColorParser.fromStringHexColor( "#faebd7" ); + static readonly aquamarine = HTMLColorParser.fromStringHexColor( "#7fffd4" ); + static readonly azure = HTMLColorParser.fromStringHexColor( "#f0ffff" ); + static readonly beige = HTMLColorParser.fromStringHexColor( "#f5f5dc" ); + static readonly bisque = HTMLColorParser.fromStringHexColor( "#ffe4c4" ); + static readonly blanchedalmond = HTMLColorParser.fromStringHexColor( "#ffebcd" ); + static readonly blueviolet = HTMLColorParser.fromStringHexColor( "#8a2be2" ); + static readonly brown = HTMLColorParser.fromStringHexColor( "#a52a2a" ); + static readonly burlywood = HTMLColorParser.fromStringHexColor( "#deb887" ); + static readonly cadetblue = HTMLColorParser.fromStringHexColor( "#5f9ea0" ); + static readonly chartreuse = HTMLColorParser.fromStringHexColor( "#7fff00" ); + static readonly chocolate = HTMLColorParser.fromStringHexColor( "#d2691e" ); + static readonly coral = HTMLColorParser.fromStringHexColor( "#ff7f50" ); + static readonly cornflowerblue = HTMLColorParser.fromStringHexColor( "#6495ed" ); + static readonly cornsilk = HTMLColorParser.fromStringHexColor( "#fff8dc" ); + static readonly crimson = HTMLColorParser.fromStringHexColor( "#dc143c" ); + static readonly cyan = HTMLColorParser.fromStringHexColor( "#00ffff" ); + static readonly darkblue = HTMLColorParser.fromStringHexColor( "#00008b" ); + static readonly darkcyan = HTMLColorParser.fromStringHexColor( "#008b8b" ); + static readonly darkgoldenrod = HTMLColorParser.fromStringHexColor( "#b8860b" ); + static readonly darkgray = HTMLColorParser.fromStringHexColor( "#a9a9a9" ); + static readonly darkgreen = HTMLColorParser.fromStringHexColor( "#006400" ); + static readonly darkgrey = HTMLColorParser.fromStringHexColor( "#a9a9a9" ); + static readonly darkkhaki = HTMLColorParser.fromStringHexColor( "#bdb76b" ); + static readonly darkmagenta = HTMLColorParser.fromStringHexColor( "#8b008b" ); + static readonly darkolivegreen = HTMLColorParser.fromStringHexColor( "#556b2f" ); + static readonly darkorange = HTMLColorParser.fromStringHexColor( "#ff8c00" ); + static readonly darkorchid = HTMLColorParser.fromStringHexColor( "#9932cc" ); + static readonly darkred = HTMLColorParser.fromStringHexColor( "#8b0000" ); + static readonly darksalmon = HTMLColorParser.fromStringHexColor( "#e9967a" ); + static readonly darkseagreen = HTMLColorParser.fromStringHexColor( "#8fbc8f" ); + static readonly darkslateblue = HTMLColorParser.fromStringHexColor( "#483d8b" ); + static readonly darkslategray = HTMLColorParser.fromStringHexColor( "#2f4f4f" ); + static readonly darkslategrey = HTMLColorParser.fromStringHexColor( "#2f4f4f" ); + static readonly darkturquoise = HTMLColorParser.fromStringHexColor( "#00ced1" ); + static readonly darkviolet = HTMLColorParser.fromStringHexColor( "#9400d3" ); + static readonly deeppink = HTMLColorParser.fromStringHexColor( "#ff1493" ); + static readonly deepskyblue = HTMLColorParser.fromStringHexColor( "#00bfff" ); + static readonly dimgray = HTMLColorParser.fromStringHexColor( "#696969" ); + static readonly dimgrey = HTMLColorParser.fromStringHexColor( "#696969" ); + static readonly dodgerblue = HTMLColorParser.fromStringHexColor( "#1e90ff" ); + static readonly firebrick = HTMLColorParser.fromStringHexColor( "#b22222" ); + static readonly floralwhite = HTMLColorParser.fromStringHexColor( "#fffaf0" ); + static readonly forestgreen = HTMLColorParser.fromStringHexColor( "#228b22" ); + static readonly gainsboro = HTMLColorParser.fromStringHexColor( "#dcdcdc" ); + static readonly ghostwhite = HTMLColorParser.fromStringHexColor( "#f8f8ff" ); + static readonly gold = HTMLColorParser.fromStringHexColor( "#ffd700" ); + static readonly goldenrod = HTMLColorParser.fromStringHexColor( "#daa520" ); + static readonly greenyellow = HTMLColorParser.fromStringHexColor( "#adff2f" ); + static readonly grey = HTMLColorParser.fromStringHexColor( "#808080" ); + static readonly honeydew = HTMLColorParser.fromStringHexColor( "#f0fff0" ); + static readonly hotpink = HTMLColorParser.fromStringHexColor( "#ff69b4" ); + static readonly indianred = HTMLColorParser.fromStringHexColor( "#cd5c5c" ); + static readonly indigo = HTMLColorParser.fromStringHexColor( "#4b0082" ); + static readonly ivory = HTMLColorParser.fromStringHexColor( "#fffff0" ); + static readonly khaki = HTMLColorParser.fromStringHexColor( "#f0e68c" ); + static readonly lavender = HTMLColorParser.fromStringHexColor( "#e6e6fa" ); + static readonly lavenderblush = HTMLColorParser.fromStringHexColor( "#fff0f5" ); + static readonly lawngreen = HTMLColorParser.fromStringHexColor( "#7cfc00" ); + static readonly lemonchiffon = HTMLColorParser.fromStringHexColor( "#fffacd" ); + static readonly lightblue = HTMLColorParser.fromStringHexColor( "#add8e6" ); + static readonly lightcoral = HTMLColorParser.fromStringHexColor( "#f08080" ); + static readonly lightcyan = HTMLColorParser.fromStringHexColor( "#e0ffff" ); + static readonly lightgoldenrodyellow = HTMLColorParser.fromStringHexColor( "#fafad2" ); + static readonly lightgray = HTMLColorParser.fromStringHexColor( "#d3d3d3" ); + static readonly lightgreen = HTMLColorParser.fromStringHexColor( "#90ee90" ); + static readonly lightgrey = HTMLColorParser.fromStringHexColor( "#d3d3d3" ); + static readonly lightpink = HTMLColorParser.fromStringHexColor( "#ffb6c1" ); + static readonly lightsalmon = HTMLColorParser.fromStringHexColor( "#ffa07a" ); + static readonly lightseagreen = HTMLColorParser.fromStringHexColor( "#20b2aa" ); + static readonly lightskyblue = HTMLColorParser.fromStringHexColor( "#87cefa" ); + static readonly lightslategray = HTMLColorParser.fromStringHexColor( "#778899" ); + static readonly lightslategrey = HTMLColorParser.fromStringHexColor( "#778899" ); + static readonly lightsteelblue = HTMLColorParser.fromStringHexColor( "#b0c4de" ); + static readonly lightyellow = HTMLColorParser.fromStringHexColor( "#ffffe0" ); + static readonly limegreen = HTMLColorParser.fromStringHexColor( "#32cd32" ); + static readonly linen = HTMLColorParser.fromStringHexColor( "#faf0e6" ); + static readonly magenta = HTMLColorParser.fromStringHexColor( "#ff00ff" ); + static readonly mediumaquamarine = HTMLColorParser.fromStringHexColor( "#66cdaa" ); + static readonly mediumblue = HTMLColorParser.fromStringHexColor( "#0000cd" ); + static readonly mediumorchid = HTMLColorParser.fromStringHexColor( "#ba55d3" ); + static readonly mediumpurple = HTMLColorParser.fromStringHexColor( "#9370db" ); + static readonly mediumseagreen = HTMLColorParser.fromStringHexColor( "#3cb371" ); + static readonly mediumslateblue = HTMLColorParser.fromStringHexColor( "#7b68ee" ); + static readonly mediumspringgreen = HTMLColorParser.fromStringHexColor( "#00fa9a" ); + static readonly mediumturquoise = HTMLColorParser.fromStringHexColor( "#48d1cc" ); + static readonly mediumvioletred = HTMLColorParser.fromStringHexColor( "#c71585" ); + static readonly midnightblue = HTMLColorParser.fromStringHexColor( "#191970" ); + static readonly mintcream = HTMLColorParser.fromStringHexColor( "#f5fffa" ); + static readonly mistyrose = HTMLColorParser.fromStringHexColor( "#ffe4e1" ); + static readonly moccasin = HTMLColorParser.fromStringHexColor( "#ffe4b5" ); + static readonly navajowhite = HTMLColorParser.fromStringHexColor( "#ffdead" ); + static readonly oldlace = HTMLColorParser.fromStringHexColor( "#fdf5e6" ); + static readonly olivedrab = HTMLColorParser.fromStringHexColor( "#6b8e23" ); + static readonly orangered = HTMLColorParser.fromStringHexColor( "#ff4500" ); + static readonly orchid = HTMLColorParser.fromStringHexColor( "#da70d6" ); + static readonly palegoldenrod = HTMLColorParser.fromStringHexColor( "#eee8aa" ); + static readonly palegreen = HTMLColorParser.fromStringHexColor( "#98fb98" ); + static readonly paleturquoise = HTMLColorParser.fromStringHexColor( "#afeeee" ); + static readonly palevioletred = HTMLColorParser.fromStringHexColor( "#db7093" ); + static readonly papayawhip = HTMLColorParser.fromStringHexColor( "#ffefd5" ); + static readonly peachpuff = HTMLColorParser.fromStringHexColor( "#ffdab9" ); + static readonly peru = HTMLColorParser.fromStringHexColor( "#cd853f" ); + static readonly pink = HTMLColorParser.fromStringHexColor( "#ffc0cb" ); + static readonly plum = HTMLColorParser.fromStringHexColor( "#dda0dd" ); + static readonly powderblue = HTMLColorParser.fromStringHexColor( "#b0e0e6" ); + static readonly rosybrown = HTMLColorParser.fromStringHexColor( "#bc8f8f" ); + static readonly royalblue = HTMLColorParser.fromStringHexColor( "#4169e1" ); + static readonly saddlebrown = HTMLColorParser.fromStringHexColor( "#8b4513" ); + static readonly salmon = HTMLColorParser.fromStringHexColor( "#fa8072" ); + static readonly sandybrown = HTMLColorParser.fromStringHexColor( "#f4a460" ); + static readonly seagreen = HTMLColorParser.fromStringHexColor( "#2e8b57" ); + static readonly seashell = HTMLColorParser.fromStringHexColor( "#fff5ee" ); + static readonly sienna = HTMLColorParser.fromStringHexColor( "#a0522d" ); + static readonly skyblue = HTMLColorParser.fromStringHexColor( "#87ceeb" ); + static readonly slateblue = HTMLColorParser.fromStringHexColor( "#6a5acd" ); + static readonly slategray = HTMLColorParser.fromStringHexColor( "#708090" ); + static readonly slategrey = HTMLColorParser.fromStringHexColor( "#708090" ); + static readonly snow = HTMLColorParser.fromStringHexColor( "#fffafa" ); + static readonly springgreen = HTMLColorParser.fromStringHexColor( "#00ff7f" ); + static readonly steelblue = HTMLColorParser.fromStringHexColor( "#4682b4" ); + static readonly tan = HTMLColorParser.fromStringHexColor( "#d2b48c" ); + static readonly thistle = HTMLColorParser.fromStringHexColor( "#d8bfd8" ); + static readonly tomato = HTMLColorParser.fromStringHexColor( "#ff6347" ); + static readonly turquoise = HTMLColorParser.fromStringHexColor( "#40e0d0" ); + static readonly violet = HTMLColorParser.fromStringHexColor( "#ee82ee" ); + static readonly wheat = HTMLColorParser.fromStringHexColor( "#f5deb3" ); + static readonly whitesmoke = HTMLColorParser.fromStringHexColor( "#f5f5f5" ); + static readonly yellowgreen = HTMLColorParser.fromStringHexColor( "#9acd32" ); + static readonly rebeccapurple = HTMLColorParser.fromStringHexColor( "#663399" ); + +} \ No newline at end of file diff --git a/browser/colors/HSLColor.ts b/browser/colors/HSLColor.ts new file mode 100644 index 0000000..ae22aad --- /dev/null +++ b/browser/colors/HSLColor.ts @@ -0,0 +1,410 @@ +import { HTMLColor } from './HTMLColor'; +import { RGBColor } from './RGBColor'; +import { MathX } from '../math/MathX'; +import { YBColorSpace } from './YBColorSpace'; + +export class HSLColor extends HTMLColor +{ + private _h = 0; + get h(){ return this._h; } + set h( value:number ) + { + this._h = value; + } + + private _s = 0; + get s(){ return this._s; } + set s( value:number ) + { + this._s = value; + } + + private _l = 0; + get l(){ return this._l; } + set l( value:number ) + { + this._l = value; + } + + // Ranges: From zero to [ 360, 100, 100, 1 ] + constructor( h:number, s:number, l:number, a:number = 1 ) + { + super(); + + this._h = h; + this._s = s; + this._l = l; + this._a = a; + } + + toHSL(){ return this;} + + clone() + { + return new HSLColor( this.h, this.s, this.l, this.a ); + } + + copyFrom( other:HTMLColor ) + { + if ( this === other ) + { + return; + } + + let otherInHSL = other.toHSL(); + + this._h = otherInHSL.h; + this._s = otherInHSL.s; + this._l = otherInHSL.l; + this._a = otherInHSL.a; + } + + shift( h:number, s:number, l:number ) + { + this._h = ( this._h + h ) % 360; + this._s = MathX.clamp( this._s + s, 0, 100 ); + this._l = MathX.clamp( this._l + l, 0, 100 ); + + return this; + } + + shiftClamped( h:number, s:number, l:number ) + { + this._h = YBColorSpace.changeHue( this._h, h ); + this._s = MathX.clamp( this._s + s, 0, 100 ); + this._l = MathX.clamp( this._l + l, 0, 100 ); + } + + shiftClampedHueBlendedFast( h:number, s:number, l:number, blendRadius:number = 20, blendStart:number = 1 ) + { + let distanceToYellow = Math.min( 1, Math.abs( this._h - 60 ) / blendRadius ); + let distanceToBlue = Math.min( 1, Math.abs( this._h - 240 ) / blendRadius ); + + this._h = YBColorSpace.changeHue( this._h, h * distanceToYellow * distanceToBlue ); + this._s = MathX.clamp( this._s + s, 0, 100 ); + this._l = MathX.clamp( this._l + l, 0, 100 ); + } + + shiftClampedHueBlended( h:number, s:number, l:number, blendRadius:number = 20, blendStart:number = 1 ) + { + let distanceToYellow = Math.abs( MathX.angleDifference( this._h, 60 ) ); + let distanceToBlue = Math.abs( MathX.angleDifference( this._h, 240 ) ); + + let newHue = YBColorSpace.changeHue( this._h, h ); + + let blendPower = 1.5; + + let cached = { hue:this._h, change:h, newHue:newHue, distanceToYellow, distanceToBlue, clampedHue:0, lerpAmount:0 }; + + if ( distanceToYellow < blendRadius || distanceToBlue < blendRadius ) + { + if ( distanceToYellow < distanceToBlue ) + { + let lerpAmount = 1; + + if ( distanceToYellow > blendStart ) + { + lerpAmount = MathX.map( distanceToYellow, blendStart, blendRadius, 1, 0 ); + } + + + newHue = MathX.lerpAngle( newHue, 60, Math.pow( lerpAmount, blendPower ) ); + cached.lerpAmount = lerpAmount; + cached.clampedHue = newHue; + + } + else + { + let lerpAmount = 1; + + if ( distanceToBlue > blendStart ) + { + lerpAmount = MathX.map( distanceToBlue, blendStart, blendRadius, 1, 0 ); + } + + newHue = MathX.lerpAngle( newHue, 240, Math.pow( lerpAmount, blendPower ) ); + } + } + + // console.log( cached ) + + this._h = newHue; + this._s = MathX.clamp( this._s + s, 0, 100 ); + this._l = MathX.clamp( this._l + l, 0, 100 ); + } + + shiftClampedHueSmoothed( h:number, s:number, l:number, kernelWidth:number = 30, kernel:number[]=[0.1,1,10,10,10,1,0.1] ) + { + let center = { x:0, y:0 }; + + let toRad = Math.PI * 2 / 360; + let toDeg = 1 / toRad; + + for ( let i = 0; i < kernel.length; i++ ) + { + let state = i / ( kernel.length - 1 ) - 0.5; + let hueOffset = state * kernelWidth; + let kernelHue = ( this._h + hueOffset ) % 360; + let hue = YBColorSpace.changeHue( kernelHue, h ); + + let x = Math.cos( hue * toRad ) * kernel[ i ]; + let y = Math.sin( hue * toRad ) * kernel[ i ]; + + center.x += x; + center.y += y; + + } + + this._h = Math.atan2( center.y, center.x ) * toDeg; + + this._s = MathX.clamp( this._s + s, 0, 100 ); + this._l = MathX.clamp( this._l + l, 0, 100 ); + } + + + + warmen( amount:number ) + { + amount = MathX.clamp( amount, 0, 180 ); + + let h = this._h; + + if ( this.isOnGreenSide() ) + { + this._h = Math.max( 60, h - amount ); + } + else + { + if ( h >= 240 ) + { + h -= 360; + } + + this._h = Math.min( 60, h + amount ); + } + + return this; + + } + + coolen( amount:number ) + { + amount = MathX.clamp( amount, 0, 180 ); + + let h = this._h; + + if ( this.isOnGreenSide() ) + { + this._h = Math.min( 240, h + amount ); + } + else + { + if ( h < 60 ) + { + h += 360; + } + + this._h = Math.max( 240, h - amount ); + } + + return this; + } + + isOnGreenSide() + { + return this._h >= 60 && this._h < 240; + } + + isOnRedSide() + { + return ! this.isOnGreenSide; + } + + shiftWarmerHue( amount:number ) + { + if ( this._h > 120 && this._h < 300 ) + { + amount = -amount; + } + + this._h = MathX.repeat( this._h + amount, 360 ); + + return this; + } + + toString():string + { + return `hsla(${this._h},${this._s}%,${this._l}%,${this._a})`; + } + + clamp():HSLColor + { + this._h = MathX.repeat( this._h, 360 ); + this._s = MathX.clamp( this._s, 0, 100 ); + this._l = MathX.clamp( this._l, 0, 100 ); + return this; + } + + equals( other:HTMLColor ) + { + if ( ! other ) + { + return false; + } + + let otherHSL = other.toHSL(); + + if ( otherHSL.h !== this.h ) + { + return false; + } + + if ( otherHSL.s !== this.s ) + { + return false; + } + + if ( otherHSL.l !== this.l ) + { + return false; + } + + if ( otherHSL.a !== this.a ) + { + return false; + } + + return true; + + } + + public toRGB( ):RGBColor + { + let s = this._s / 100; + let l = this._l / 100; + + let m2 = ( l <= 0.5 ) ? + ( l * ( 1 + s ) ) : + ( l + s - l * s ); + + let m1 = 2 * l - m2; + + if ( s == 0 ) + { + return new RGBColor( l, l, l, this.a ); + } + + let r = HSLColor.computeChannel( m1, m2, this.h + 120 ); + let g = HSLColor.computeChannel( m1, m2, this.h ); + let b = HSLColor.computeChannel( m1, m2, this.h - 120 ); + + return new RGBColor( r, g, b, this.a ); + } + + private static computeChannel( n1:number, n2:number, hue:number ):number + { + hue = MathX.repeat( hue, 360 ); + + if ( hue < 60 ) + { + return n1 + ( n2 - n1 ) * hue / 60; + } + else if ( hue < 180 ) + { + return n2; + } + else if ( hue < 240 ) + { + return n1 + ( n2 - n1 ) * ( 240 - hue ) / 60; + } + else + { + return n1; + } + } + + public static fromRGBColor( c:RGBColor ):HSLColor + { + + let cmin = Math.min( Math.min( c.r, c.g ), c.b ); + let cmax = Math.max( Math.max( c.r, c.g ), c.b ); + + let l = ( cmin + cmax ) / 2; + + if ( cmin == cmax ) + { + return new HSLColor( 0, 0, l * 100, c.a ); + } + + let delta = cmax - cmin; + + let s = ( l <= .5 ) ? + ( delta / ( cmax + cmin ) ) : + ( delta / ( 2 - ( cmax + cmin ) ) ); + + let h = 0; + + if ( c.r == cmax ) + { + h = ( c.g - c.b ) / delta; + } + else if ( c.g == cmax ) + { + h = 2 + ( c.b - c.r ) / delta; + } + else if ( c.b == cmax ) + { + h = 4 + ( c.r - c.g ) / delta; + } + + h = MathX.repeat( h * 60, 360 ); + + return new HSLColor( h, s * 100, l * 100, c.a ); + } + + static fromString( colorString:string ) + { + colorString = colorString.trim(); + + let hslStart = /hsla?\(/; + + if ( ! hslStart.test( colorString ) ) + { + console.error( "Does not start with", hslStart, " ==> '" + colorString + "'" ); + return null; + } + + colorString = colorString.replace( hslStart, "" ); + colorString = colorString.replace( /\)$/, "" ); + colorString = colorString.replace( /%/g, "" ); + + + let numbers = JSON.parse( "[" + colorString + "]" ) as number[]; + + if ( ! Array.isArray( numbers ) ) + { + console.error( "Is not a list of numbers", " ==> '" + colorString + "'" ); + return null; + } + + let notNumeric = numbers.findIndex( n => typeof n !== "number" ); + + if ( notNumeric !== -1 ) + { + console.error( "Found a not-numeric at index:", notNumeric, " ==> '" + colorString + "'" ); + return null; + } + + if ( ! ( numbers.length === 3 || numbers.length === 4 ) ) + { + return null; + } + + if ( numbers.length === 3 ) + { + numbers.push( 1 ); + } + + return new HSLColor( numbers[ 0 ], numbers[ 1 ], numbers[ 2 ] , numbers[ 3 ] ); + } +} diff --git a/browser/colors/HTMLColor.ts b/browser/colors/HTMLColor.ts new file mode 100644 index 0000000..7c8ce59 --- /dev/null +++ b/browser/colors/HTMLColor.ts @@ -0,0 +1,145 @@ +import { RGBColor } from './RGBColor'; +import { HSLColor } from './HSLColor'; +import { MathX } from '../math/MathX'; + +export abstract class HTMLColor +{ + protected _a = 0; + get a(){ return this._a; } + set a( value:number ){ this._a = value; } + + abstract toRGB():RGBColor; + abstract toHSL():HSLColor; + abstract copyFrom( color:HTMLColor ):void; + abstract clone():HTMLColor; + + get r() + { + return this.toRGB().r; + } + + set r( value:number ) + { + let rgb = this.toRGB(); + rgb.r = value; + this.copyFrom( rgb ); + } + + get g() + { + return this.toRGB().g; + } + + set g( value:number ) + { + let rgb = this.toRGB(); + rgb.g = value; + this.copyFrom( rgb ); + } + + get b() + { + return this.toRGB().b; + } + + set b( value:number ) + { + let rgb = this.toRGB(); + rgb.b = value; + this.copyFrom( rgb ); + } + + get h() + { + return this.toHSL().h; + } + + set h( value:number ) + { + let hsl = this.toHSL(); + hsl.h = value; + this.copyFrom( hsl ); + } + + get s() + { + return this.toHSL().s; + } + + set s( value:number ) + { + let hsl = this.toHSL(); + hsl.s = value; + this.copyFrom( hsl ); + } + + get l() + { + return this.toHSL().l; + } + + set l( value:number ) + { + let hsl = this.toHSL(); + hsl.l = value; + this.copyFrom( hsl ); + } + + lighten( amount:number ):HTMLColor + { + this.l = this.l + amount; + return this; + } + + multiplyLuminance( amount:number ):HTMLColor + { + this.l = this.l * amount; + return this; + } + + + saturate( amount:number ):HTMLColor + { + this.s = this.s + amount; + return this; + } + + shiftHue( amount:number ):HTMLColor + { + this.h = MathX.repeat( this.h + amount, 360 ); + return this; + } + + fade( amount:number ):HTMLColor + { + this.a = this.a + amount; + return this; + } + + get physicalLightness() + { + return this.r * 0.3 + this.g * 0.5 + this.b * 0.2; + } + + setPhysicalLightness( target:number, amount:number ) + { + let rgb = this.toRGB(); + let lightness = rgb.physicalLightness; + let scale = target / lightness; + scale = scale * amount + ( 1 - amount ); + + rgb.scaleRGB( scale ); + + this.copyFrom( rgb ); + + return this; + } + + + equals( other:HTMLColor ) + { + return this.toHSL().equals( other ); + } + + +} \ No newline at end of file diff --git a/browser/colors/HTMLColorNameTable.ts b/browser/colors/HTMLColorNameTable.ts new file mode 100644 index 0000000..2e95621 --- /dev/null +++ b/browser/colors/HTMLColorNameTable.ts @@ -0,0 +1,166 @@ +export class HTMLColorNameTable +{ + static getHexColorFromName( name:string ) + { + return HTMLColorNameTable.data[ name.toLocaleLowerCase() ]; + } + + static contains( name:string ) + { + let value = HTMLColorNameTable.data[ name ]; + return typeof value === "string"; + } + + static readonly data:any = + { + "black":"#000000", + "silver":"#c0c0c0", + "gray":"#808080", + "white":"#ffffff", + "maroon":"#800000", + "red":"#ff0000", + "purple":"#800080", + "fuchsia":"#ff00ff", + "green":"#008000", + "lime":"#00ff00", + "olive":"#808000", + "yellow":"#ffff00", + "navy":"#000080", + "blue":"#0000ff", + "teal":"#008080", + "aqua":"#00ffff", + "orange":"#ffa500", + "aliceblue":"#f0f8ff", + "antiquewhite":"#faebd7", + "aquamarine":"#7fffd4", + "azure":"#f0ffff", + "beige":"#f5f5dc", + "bisque":"#ffe4c4", + "blanchedalmond":"#ffebcd", + "blueviolet":"#8a2be2", + "brown":"#a52a2a", + "burlywood":"#deb887", + "cadetblue":"#5f9ea0", + "chartreuse":"#7fff00", + "chocolate":"#d2691e", + "coral":"#ff7f50", + "cornflowerblue":"#6495ed", + "cornsilk":"#fff8dc", + "crimson":"#dc143c", + "cyan":"#00ffff", + "darkblue":"#00008b", + "darkcyan":"#008b8b", + "darkgoldenrod":"#b8860b", + "darkgray":"#a9a9a9", + "darkgreen":"#006400", + "darkgrey":"#a9a9a9", + "darkkhaki":"#bdb76b", + "darkmagenta":"#8b008b", + "darkolivegreen":"#556b2f", + "darkorange":"#ff8c00", + "darkorchid":"#9932cc", + "darkred":"#8b0000", + "darksalmon":"#e9967a", + "darkseagreen":"#8fbc8f", + "darkslateblue":"#483d8b", + "darkslategray":"#2f4f4f", + "darkslategrey":"#2f4f4f", + "darkturquoise":"#00ced1", + "darkviolet":"#9400d3", + "deeppink":"#ff1493", + "deepskyblue":"#00bfff", + "dimgray":"#696969", + "dimgrey":"#696969", + "dodgerblue":"#1e90ff", + "firebrick":"#b22222", + "floralwhite":"#fffaf0", + "forestgreen":"#228b22", + "gainsboro":"#dcdcdc", + "ghostwhite":"#f8f8ff", + "gold":"#ffd700", + "goldenrod":"#daa520", + "greenyellow":"#adff2f", + "grey":"#808080", + "honeydew":"#f0fff0", + "hotpink":"#ff69b4", + "indianred":"#cd5c5c", + "indigo":"#4b0082", + "ivory":"#fffff0", + "khaki":"#f0e68c", + "lavender":"#e6e6fa", + "lavenderblush":"#fff0f5", + "lawngreen":"#7cfc00", + "lemonchiffon":"#fffacd", + "lightblue":"#add8e6", + "lightcoral":"#f08080", + "lightcyan":"#e0ffff", + "lightgoldenrodyellow":"#fafad2", + "lightgray":"#d3d3d3", + "lightgreen":"#90ee90", + "lightgrey":"#d3d3d3", + "lightpink":"#ffb6c1", + "lightsalmon":"#ffa07a", + "lightseagreen":"#20b2aa", + "lightskyblue":"#87cefa", + "lightslategray":"#778899", + "lightslategrey":"#778899", + "lightsteelblue":"#b0c4de", + "lightyellow":"#ffffe0", + "limegreen":"#32cd32", + "linen":"#faf0e6", + "magenta":"#ff00ff", + "mediumaquamarine":"#66cdaa", + "mediumblue":"#0000cd", + "mediumorchid":"#ba55d3", + "mediumpurple":"#9370db", + "mediumseagreen":"#3cb371", + "mediumslateblue":"#7b68ee", + "mediumspringgreen":"#00fa9a", + "mediumturquoise":"#48d1cc", + "mediumvioletred":"#c71585", + "midnightblue":"#191970", + "mintcream":"#f5fffa", + "mistyrose":"#ffe4e1", + "moccasin":"#ffe4b5", + "navajowhite":"#ffdead", + "oldlace":"#fdf5e6", + "olivedrab":"#6b8e23", + "orangered":"#ff4500", + "orchid":"#da70d6", + "palegoldenrod":"#eee8aa", + "palegreen":"#98fb98", + "paleturquoise":"#afeeee", + "palevioletred":"#db7093", + "papayawhip":"#ffefd5", + "peachpuff":"#ffdab9", + "peru":"#cd853f", + "pink":"#ffc0cb", + "plum":"#dda0dd", + "powderblue":"#b0e0e6", + "rosybrown":"#bc8f8f", + "royalblue":"#4169e1", + "saddlebrown":"#8b4513", + "salmon":"#fa8072", + "sandybrown":"#f4a460", + "seagreen":"#2e8b57", + "seashell":"#fff5ee", + "sienna":"#a0522d", + "skyblue":"#87ceeb", + "slateblue":"#6a5acd", + "slategray":"#708090", + "slategrey":"#708090", + "snow":"#fffafa", + "springgreen":"#00ff7f", + "steelblue":"#4682b4", + "tan":"#d2b48c", + "thistle":"#d8bfd8", + "tomato":"#ff6347", + "turquoise":"#40e0d0", + "violet":"#ee82ee", + "wheat":"#f5deb3", + "whitesmoke":"#f5f5f5", + "yellowgreen":"#9acd32", + "rebeccapurple":"#663399" + } + +} diff --git a/browser/colors/HTMLColorParser.ts b/browser/colors/HTMLColorParser.ts new file mode 100644 index 0000000..e3833b8 --- /dev/null +++ b/browser/colors/HTMLColorParser.ts @@ -0,0 +1,53 @@ +import { HTMLColorNameTable } from './HTMLColorNameTable'; +import { HTMLColor } from './HTMLColor'; +import { RGBColor } from './RGBColor'; +import { HSLColor } from './HSLColor'; + +export class HTMLColorParser +{ + static fromString( colorString:string ) + { + colorString = colorString.trim(); + + let hexMatcher = /^#[0-9a-f]{6}([0-9a-f][0-9a-f])?$/i; + let rgbMatcher = /^rgba?\(/; + let hslMatcher = /^hsla?\(/; + + + if ( HTMLColorNameTable.contains( colorString ) ) + { + colorString = HTMLColorNameTable.getHexColorFromName( colorString ); + } + + if ( hexMatcher.test( colorString ) ) + { + return HTMLColorParser.fromStringHexColor( colorString ); + } + else if ( rgbMatcher.test ( colorString ) ) + { + return RGBColor.fromString( colorString ); + } + else if ( hslMatcher.test( colorString ) ) + { + return HSLColor.fromString( colorString ); + } + + console.warn( `Couldn't parse: "${colorString}"`) + return null; + + } + + static fromStringHexColor( colorString:string ) + { + colorString = colorString.replace( "#", "" ); + + let r = parseInt( colorString.substring( 0, 2 ), 16 ); + let g = parseInt( colorString.substring( 2, 4 ), 16 ); + let b = parseInt( colorString.substring( 4, 6 ), 16 ); + + let a = colorString.length === 6 ? + 255 : parseInt( colorString.substring( 6, 8 ), 16 ); + + return new RGBColor( r / 255, g / 255, b / 255, a / 255 ); + } +} \ No newline at end of file diff --git a/browser/colors/RGBColor.ts b/browser/colors/RGBColor.ts new file mode 100644 index 0000000..5129d71 --- /dev/null +++ b/browser/colors/RGBColor.ts @@ -0,0 +1,221 @@ +import { HTMLColor } from './HTMLColor'; +import { HSLColor } from './HSLColor'; + +export class RGBColor extends HTMLColor +{ + private _r = 0; + get r(){ return this._r} + set r( value:number ) + { + this._r = value; + } + + private _g = 0; + get g(){ return this._g} + set g( value:number ) + { + this._g = value; + } + + private _b = 0; + get b(){ return this._b} + set b( value:number ) + { + this._b = value; + } + + constructor( r:number, g:number, b:number, a:number=1 ) + { + super(); + + this._r = r; + this._g = g; + this._b = b; + this._a = a; + } + + clone() + { + return new RGBColor( this.r, this.g, this.b, this.a ); + } + + copyFrom( other:HTMLColor ) + { + if ( this === other ) + { + return; + } + + let otherInRGB = other.toRGB(); + + this._r = otherInRGB.r; + this._g = otherInRGB.g; + this._b = otherInRGB.b; + this._a = otherInRGB.a; + } + + toRGB():RGBColor + { + return this; + } + + toHSL():HSLColor + { + return HSLColor.fromRGBColor( this ); + } + + get r255() + { + return Math.round( this._r * 255 ); + } + + get g255() + { + return Math.round( this._g * 255 ); + } + + get b255() + { + return Math.round( this._b * 255 ); + } + + toString():string + { + return `rgba(${this.r255},${this.g255},${this.b255},${this._a})`; + } + + scaleRGB( scalar:number ) + { + this.r *= scalar; + this.g *= scalar; + this.b *= scalar; + } + + getEuclideanDistance( color:HTMLColor ) + { + let rgb = color.toRGB(); + + let rDiff = rgb.r - this.r; + let gDiff = rgb.g - this.g; + let bDiff = rgb.b - this.b; + let aDiff = rgb.a - this.a; + + return Math.sqrt( rDiff * rDiff + gDiff * gDiff + bDiff * bDiff + aDiff * aDiff ); + } + + static lerp( colorA:HTMLColor, colorB:HTMLColor, lerp:number ):RGBColor + { + let A = colorA.toRGB(); + let B = colorB.toRGB(); + + let inverseLerp = 1 - lerp; + + let r = A.r * inverseLerp + B.r * lerp; + let g = A.g * inverseLerp + B.g * lerp; + let b = A.b * inverseLerp + B.b * lerp; + let a = A.a * inverseLerp + B.a * lerp; + + return new RGBColor( r, g, b, a ); + } + + static lerpFrom( colors:HTMLColor[], lerp:number ):RGBColor + { + if ( lerp <= 0 ) + { + return colors[ 0 ].toRGB(); + } + + if ( lerp >= 1 ) + { + return colors[ colors.length - 1 ].toRGB(); + } + + let index = lerp * ( colors.length - 1 ); + let floored = Math.floor( index ); + + let amount = index - floored; + + + let colorA = colors[ floored ]; + let colorB = colors[ floored + 1 ]; + + return RGBColor.lerp( colorA, colorB, amount ); + } + + equals( other:HTMLColor ) + { + if ( ! other ) + { + return false; + } + + let otherRGB = other.toRGB(); + + if ( otherRGB.r !== this.r ) + { + return false; + } + + if ( otherRGB.g !== this.g ) + { + return false; + } + + if ( otherRGB.b !== this.b ) + { + return false; + } + + if ( otherRGB.a !== this.a ) + { + return false; + } + + return true; + + } + + static fromString( colorString:string ) + { + colorString = colorString.trim(); + + let rgbStart = /rgba?\(/; + + if ( ! rgbStart.test( colorString ) ) + { + console.error( "Does not start with", rgbStart, " ==> '" + colorString + "'" ); + return null; + } + + colorString = colorString.replace( rgbStart, "" ); + colorString = colorString.replace( /\)$/, "" ); + + let numbers = JSON.parse( "[" + colorString + "]" ) as number[]; + + if ( ! Array.isArray( numbers ) ) + { + console.error( "Is not a list of numbers", " ==> '" + colorString + "'" ); + return null; + } + + let notNumeric = numbers.findIndex( n => typeof n !== "number" ); + + if ( notNumeric !== -1 ) + { + console.error( "Found a not-numeric at index:", notNumeric, " ==> '" + colorString + "'" ); + return null; + } + + if ( ! ( numbers.length === 3 || numbers.length === 4 ) ) + { + return null; + } + + if ( numbers.length === 3 ) + { + numbers.push( 1 ); + } + + return new RGBColor( numbers[ 0 ] / 255, numbers[ 1 ] / 255, numbers[ 2 ] / 255, numbers[ 3 ] ); + } +} \ No newline at end of file diff --git a/browser/colors/YBColorSpace.ts b/browser/colors/YBColorSpace.ts new file mode 100644 index 0000000..79cc885 --- /dev/null +++ b/browser/colors/YBColorSpace.ts @@ -0,0 +1,29 @@ +import { MathX } from "../math/MathX"; + +export class YBColorSpace +{ + static hueToYB( hue:number ) + { + return MathX.repeatPolar( hue - 240, 180 ); + } + + static ybToHue( yb:number ) + { + return MathX.repeat( yb + 240, 360 ); + } + + static changeHue( hue:number, change:number ) + { + let yb = YBColorSpace.hueToYB( hue ); + + let absYB = Math.abs( yb ); + + absYB = MathX.clamp( absYB + change, 0, 180 ); + + let realYB = yb < 0 ? -absYB : absYB; + + return this.ybToHue( realYB ); + } + + +} diff --git a/browser/dom/ClassFlag.ts b/browser/dom/ClassFlag.ts new file mode 100644 index 0000000..22c4ad6 --- /dev/null +++ b/browser/dom/ClassFlag.ts @@ -0,0 +1,232 @@ +import { RegExpUtility } from "../text/RegExpUtitlity"; +import { DOMEditor } from "./DOMEditor"; + +export class ClassFlag +{ + static readonly active = new ClassFlag( "active" ); + static readonly inactive = new ClassFlag( "inactive" ); + + static readonly enabled = new ClassFlag( "enabled" ); + static readonly disabled = new ClassFlag( "disabled" ); + + static readonly button = new ClassFlag( "button" ); + + static readonly visible = new ClassFlag( "visible" ); + static readonly invisible = new ClassFlag( "invisible" ); + + static readonly hidden = new ClassFlag( "hidden" ); + static readonly show = new ClassFlag( "show" ); + static readonly hideable = new ClassFlag( "hideable" ); + + static readonly playing = new ClassFlag( "playing" ); + static readonly paused = new ClassFlag( "paused" ); + + static readonly error = new ClassFlag( "error" ); + static readonly warning = new ClassFlag( "warning" ); + static readonly ok = new ClassFlag( "ok" ); + + static readonly debugging = new ClassFlag( "debugging" ); + + static readonly loggedIn = new ClassFlag( "logged-in" ); + static readonly loggedOut = new ClassFlag( "logged-out" ); + + static readonly hovered = new ClassFlag( "hovered" ); + static readonly selected = new ClassFlag( "selected" ); + static readonly dragging = new ClassFlag( "dragging" ); + + static readonly smooth = new ClassFlag( "smooth" ); + + static readonly debug = new ClassFlag( "debug" ); + + static readonly open = new ClassFlag( "open" ); + + static readonly default = new ClassFlag( "default" ); + + static readonly clickable = new ClassFlag( "clickable" ); + + private _value:string; + + constructor( value:string ) + { + this._value = value; + } + + static fromEnum( value:string, prefix = "", appendix = "" ) + { + let flag = value.toLowerCase().replace( "_", "-" ); + + return new ClassFlag( prefix + flag + appendix ); + } + + get selector() + { + return "." + this._value; + } + + query( element:Element ) + { + return element.querySelector( this.selector ); + } + + queryAll( element:Element ) + { + return DOMEditor.nodeListToArray( element.querySelectorAll( this.selector ) ); + } + + forEachInDoc( callback:( n:Element ) => void ) + { + let all = this.queryAll( document.body ); + + for ( let i = 0; i < all.length; i++) + { + callback( all[ i ] ); + } + } + + forEachIn( source:Element, callback:( n:Element ) => void ) + { + let all = this.queryAll( source); + + for ( let i = 0; i < all.length; i++) + { + callback( all[ i ] ); + } + } + + queryPrefixed( element:Element ) + { + return element.querySelector( `[class^="${this._value}"]` ); + } + + queryPrefixedAll( element:Element ) + { + return element.querySelectorAll( `[class^="${this._value}"]` ); + } + + queryDoc() + { + return document.querySelector( this.selector ); + } + + set( element:Element ) + { + ClassFlag.setClass( element, this._value, true ); + return element; + } + + setAll( elements:Element[], selectedElement:Element ) + { + elements.forEach( e => this.setAs( e, e === selectedElement ) ); + } + + setSolo( element:Element ) + { + element.setAttribute( "class", this._value ); + } + + setAs( element:Element, value:boolean ) + { + ClassFlag.setClass( element, this._value, value ); + return element; + } + + removeFrom( element:Element ) + { + ClassFlag.setClass( element, this._value, false ); + return element; + } + + toggle( element:Element ) + { + ClassFlag.toggleClass( element, this._value ); + return element; + } + + in( element:Element ) + { + return ClassFlag.hasClass( element, this._value ); + } + + static clear( element: Element ) + { + element.removeAttribute( "class" ); + } + + static assign( element:Element, value:string ) + { + new ClassFlag( value ).set( element ); + } + + static addClass( element:Element, classString:string ) + { + let classAttribute = element.getAttribute( "class" ) || ""; + let matcher = RegExpUtility.createWordMatcher( classString ); + + if ( matcher.test( classAttribute ) ) + { + return; + } + + classAttribute += " " + classString; + + ClassFlag.setNormalizedClassAttribute( element, classAttribute ); + } + + static removeClass( element:Element, classString:string ) + { + let matcher = RegExpUtility.createWordMatcher( classString ); + let classAttribute = element.getAttribute( "class" ) || ""; + + classAttribute = classAttribute.replace( matcher, "" ); + ClassFlag.setNormalizedClassAttribute( element, classAttribute ); + } + + static hasClass( element:Element, classString:string ) + { + let matcher = RegExpUtility.createWordMatcher( classString ); + let classAttribute = element.getAttribute( "class" ) || ""; + + return matcher.test( classAttribute ); + } + + static toggleClass( element:Element, className:string ) + { + let flag = ! ClassFlag.hasClass( element, className ); + ClassFlag.setClass( element, className, flag ); + } + + static setNormalizedClassAttribute( element:Element, classAttribute:string ) + { + classAttribute = classAttribute.replace( /\s+/g, " " ); + classAttribute = classAttribute.trim(); + + if ( "" === classAttribute ) + { + element.removeAttribute( "class" ); + } + else + { + element.setAttribute( "class", classAttribute ); + } + + } + + static setClass( element:Element, classString:string, enable:boolean ) + { + if ( enable ) + { + ClassFlag.addClass( element, classString ); + } + else + { + ClassFlag.removeClass( element, classString ); + } + } + + static toggleClassState( element:Element, classString:string ) + { + let hasClassAssigned = ClassFlag.hasClass( element, classString ); + + ClassFlag.setClass( element, classString, ! hasClassAssigned ); + } +} \ No newline at end of file diff --git a/browser/dom/DOMEditor.ts b/browser/dom/DOMEditor.ts new file mode 100644 index 0000000..7c15365 --- /dev/null +++ b/browser/dom/DOMEditor.ts @@ -0,0 +1,162 @@ +export class DOMEditor +{ + static nodeListToArray( list:NodeList ) + { + return Array.prototype.slice.call( list ); + } + + static prefixData( source:string ) + { + let singleReplaceRegex = /\[\s*-/g; + source = source.replace(singleReplaceRegex, "[data-"); + let autoDataExtensionRegex = /\[(?!data-|([a-zA-Z0-9]+s*(\=|\~|\||\^|\$|\*|\])))/g; + return source.replace( autoDataExtensionRegex, "[data-" ); + } + + static copyAttribute( destination:Element, source:Element, sourceAttribute:string) + { + sourceAttribute = DOMEditor.prefixData( sourceAttribute ); + let value = source.getAttribute (sourceAttribute ); + destination.setAttribute( sourceAttribute, value ); + } + + static copyAllAttributesThroughRawHTML( source:Element, destination:Element ) + { + let attributes:[string,string][] = []; + + for ( let i = 0; i < destination.attributes.length; i++ ) + { + let attribute = destination.attributes[ i ]; + let attributeName = attribute.nodeName; + let attributeValue = attribute.nodeValue; + + attributes.push( [ attributeName, attributeValue ] ); + } + + for ( let i = 0; i < source.attributes.length; i++ ) + { + let attribute = source.attributes[ i ]; + let attributeName = attribute.nodeName; + let attributeValue = attribute.nodeValue; + + let index = attributes.findIndex( a => a[ 0 ] === attributeName ); + + if ( index === -1 ) + { + attributes.push( [ attributeName, attributeValue ] ); + } + else + { + attributes[ index ][ 1 ] = attributeValue; + } + } + + let innerHTML = destination.innerHTML; + let attributesString = ""; + + for ( let i = 0; i < attributes.length; i++ ) + { + if ( i !== 0 ){ attributesString += " "; } + + let attribute = attributes[ i ]; + attributesString += `${attribute[ 0 ]}="${attribute[ 1 ]}"`; + } + + let tag = destination.nodeName.toLowerCase(); + + let html = `<${tag} ${attributesString}>${innerHTML}`; + + console.log( "Copied html:", html ); + + destination.outerHTML = html; + + return destination; + + } + + static copyAllAttributes( source:Element, destination:Element ) + { + for ( let i = 0; i < source.attributes.length; i++ ) + { + let attribute = source.attributes[ i ]; + let attributeName = attribute.nodeName; + let attributeValue = attribute.nodeValue; + destination.setAttribute( attributeName, attributeValue ); + } + } + + static cloneChildren( element:Element ):Node[] + { + let clones:Node[] = []; + + for ( let i = 0; i < element.childNodes.length; i++ ) + { + + clones.push( element.childNodes[ i ].cloneNode( true ) ); + } + + + return clones; + } + + static _cssCreationRegex:RegExp; + + static get cssCreationRegex() + { + if ( DOMEditor._cssCreationRegex) + { + return DOMEditor._cssCreationRegex; + } + + let cssCreationRules = [ + //"Attribute", + /\[\s*(?:\w|-)+\s*(?:(?:\~|\||\^|\$|\*)?=)\s*(?:(?:"(?:\\"|.)*?")|(?:'(?:\\'|.)*?')|(?:(?:\w|-)+))\s*\]/, + //"ID", + /#(?:\w|-)+/, + //"Class", + /\.(?:\w|-)+/, + //"Empty Attribute", + /\[\s*(?:\w|-)+\s*]/, + //"Element", + /(?:\w|-)+/ + ]; + + let regexString = ""; + + for (let i = 0; i < cssCreationRules.length; i++) + { + if (i !== 0) + { + regexString += "|"; + } + + regexString += "(" + cssCreationRules[i].source + ")"; + } + + DOMEditor._cssCreationRegex = new RegExp(regexString, "g"); + + return DOMEditor._cssCreationRegex; + } + + static stringToDocument( html:string ):Document + { + let parser = new DOMParser(); + let doc = parser.parseFromString( html, "text/html" ); + return doc; + } + + static remove( node:Node ) + { + if ( ! node ) + { + return; + } + + if ( ! node.parentNode ) + { + return; + } + + node.parentNode.removeChild( node ); + } +} \ No newline at end of file diff --git a/browser/dom/DOMNameSpaces.ts b/browser/dom/DOMNameSpaces.ts new file mode 100644 index 0000000..340b9d2 --- /dev/null +++ b/browser/dom/DOMNameSpaces.ts @@ -0,0 +1,5 @@ +export class DOMNameSpaces +{ + static readonly SVG = 'http://www.w3.org/2000/svg'; + static readonly XLink = 'http://www.w3.org/1999/xlink'; +} \ No newline at end of file diff --git a/browser/dom/ElementAttribute.ts b/browser/dom/ElementAttribute.ts new file mode 100644 index 0000000..5856b1e --- /dev/null +++ b/browser/dom/ElementAttribute.ts @@ -0,0 +1,412 @@ +import { HTMLNodeTreeWalker } from "../graphs/HTMLNodeTreeWalker"; +import { DOMEditor } from "./DOMEditor"; +import { DOMNameSpaces } from "./DOMNameSpaces"; + +export class ElementAttribute +{ + static readonly nameAttribute = new ElementAttribute( "name", false ); + static readonly style = new ElementAttribute( "style", false ); + static readonly class = new ElementAttribute( "class", false ); + static readonly id = new ElementAttribute( "id", false ); + static readonly href = new ElementAttribute( "href", false ); + static readonly download = new ElementAttribute( "download", false ); + static readonly target = new ElementAttribute( "target", false ); + static readonly type = new ElementAttribute( "type", false ); + static readonly lang = new ElementAttribute( "lang", false ); + static readonly src = new ElementAttribute( "src", false ); + static readonly value = new ElementAttribute( "value", false ); + + static readonly svgClass = new ElementAttribute( "class", false, "" ); + static readonly xlinkHref = new ElementAttribute( "xlink:href", false, DOMNameSpaces.XLink ); + + static readonly contentEditable = new ElementAttribute( "contenteditable", false ); + static readonly allowfullscreen = new ElementAttribute( "allowfullscreen", false ); + static readonly controls = new ElementAttribute( "controls", false ); + static readonly preload = new ElementAttribute( "preload", false ); + + static readonly selected = new ElementAttribute( "selected", false ); + + static readonly data_lang = new ElementAttribute( "lang" ); + + static readonly title = new ElementAttribute( "title", false ); + static readonly width = new ElementAttribute( "width", false ); + static readonly height = new ElementAttribute( "height", false ); + static readonly frameborder = new ElementAttribute( "frameborder", false ); + static readonly allow = new ElementAttribute( "allow", false ); + static readonly autoplay = new ElementAttribute( "autoplay", false ); + static readonly muted = new ElementAttribute( "muted", false ); + static readonly loop = new ElementAttribute( "loop", false ); + static readonly readonly = new ElementAttribute( "readonly", false ); + + static readonly dataType = new ElementAttribute( "type" ); + static readonly dataName = new ElementAttribute( "name" ); + + static readonly hidden = new ElementAttribute( "hidden", false ); + + static readonly rootHref = new ElementAttribute( "root-href" ); + + private _name:string; + get name(){ return this._name; } + + get fullName() + { + return this._useDataPrefix ? ( "data-" + this._name ) : this._name; + } + + private _useDataPrefix = true; + + private _useNameSpace = false; + get usesNameSpace(){ return this._useNameSpace; } + + private _nameSpace:string = null; + get nameSpace(){ return this._nameSpace;} + + constructor( name:string, useDataPrefix:boolean = true, nameSpace:string = null ) + { + if ( this._name && this._name.startsWith( "data-" ) && useDataPrefix ) + { + console.warn( + `The name of the attribute starts with a 'data-' prefix AND the 'useDataPrefix' at the same time!` ); + } + + this._name = name; + this._useDataPrefix = useDataPrefix; + + if ( nameSpace ) + { + this._useNameSpace = true; + this._nameSpace = nameSpace === "" ? null : nameSpace; + } + } + + forAll( target:Element|Document, callback:(element:Element)=>any) + { + let elements = target.querySelectorAll( this.selector ); + + for ( let i = 0; i < elements.length; i++ ) + { + callback( elements[ i ] ); + } + } + + + toString() + { + return this.fullName; + } + + queryDoc() + { + return document.querySelector( this.selector ); + } + + + querySelfOrParent( element:Element ) + { + if ( this.in( element ) ) + { + return element; + } + + return this.queryParent( element ); + } + + + queryParent( element:Element ) + { + let parent = element.parentElement; + + while ( parent ) + { + if ( this.in( parent ) ) + { + return parent; + } + + parent = parent.parentElement; + } + + return null; + + } + + queryParentValue( element:Element ) + { + let parent = this.queryParent( element ); + return this.from( parent ); + } + + queryWithValue( element:Element|Document, value:string ) + { + return element.querySelector( this.selectorEquals( value ) ); + } + + queryDocWithValue( value:string ) + { + return this.queryWithValue( document, value ); + } + + hasParentWithValue( element:Element, value:string ) + { + return this.queryParentValue( element ) === value; + } + + query( element:Element|Document ) + { + return element.querySelector( this.selector ) as any as T; + } + + isQueriedChecked( element:Element ) + { + let queried = this.query( element ); + + if ( ! queried ) + { + return false; + } + + return queried.checked; + } + + + matches( element:Element, regex:RegExp ) + { + if ( ! this.in( element ) ) + { + return false; + } + + return regex.exec( this.from( element ) ) !== null; + } + + queryDocAll() + { + return DOMEditor.nodeListToArray( document.querySelectorAll( this.selector ) ); + } + + forEachInDoc( callback:(element:Element)=>void ) + { + this.queryDocAll().forEach( callback ); + } + + queryAll( t:Element|Document ) + { + if ( ! this._needsWalker( this.selector ) ) + { + return this._queryAllQuerySelector( t ); + } + + return this._queryAllWalker( t ); + + } + + _needsWalker( selector:string ) + { + if ( "[xlink:href]" == selector ) + { + return true; + } + + return false; + } + + + _queryAllQuerySelector( element:Element|Document ) + { + return DOMEditor.nodeListToArray( element.querySelectorAll( this.selector ) ); + } + + _queryAllWalker( t:Element|Document ) + { + let list:Element[] = []; + let walker = new HTMLNodeTreeWalker(); + let end = walker.iterationEndOf( t ); + let it:Node = t; + + while ( it !== end ) + { + if ( Node.ELEMENT_NODE != it.nodeType ) + { + it = walker.nextNode( it ); + continue; + } + + let element = it as Element; + + if ( this.in( element ) ) + { + list.push( element ); + } + + it = walker.nextNode( it ); + } + + return list; + } + + queryValueInDoc( element:Element ) + { + return this.queryValueIn( element, document ); + } + + queryValueIn( element:Element, source:Document|Element = undefined ) + { + source = source || element; + + let selector = this.from( element ); + return source.querySelector( selector ); + } + + + get selector() + { + return `[${this.fullName}]`; + } + + selectorContaining( fragment:string ) + { + return `[${this.fullName}*="${fragment}"]`; + } + + selectorContainingWord( word:string ) + { + return `[${this.fullName}|="${word}"]`; + } + + selectorStartsWith( word:string ) + { + return `[${this.fullName}^="${word}"]`; + } + + selectorEquals( word:string ) + { + let selector = `[${this.fullName}="${word}"]`; + return selector; + } + + is( element:Element, value:string|RegExp ) + { + if ( ! this.in( element ) ) + { + return false; + } + + let stringValue = this.from( element ); + + if ( typeof value === "string" ) + { + return value === stringValue; + } + + return value.test( stringValue ); + + + } + + + in( element:Element ) + { + if ( ! element.attributes ) + { + return false; + } + + if ( this._useNameSpace ) + { + return element.hasAttributeNS( this._nameSpace, this.fullName ); + } + + return element.hasAttribute( this.fullName ); + } + + inAll( elements:Element[] ) + { + if ( elements.length === 0 ) + { + return false; + } + + for ( let e of elements ) + { + if ( ! this.in( e ) ) + { + return false; + } + } + + return true; + } + + from( element:Element, alternative:string = null ) + { + if ( ! element ) + { + console.log( "Element is null", this ); + return null; + } + + if ( this._useNameSpace ) + { + return element.getAttributeNS( this._nameSpace, this.fullName ); + } + + return element.getAttribute( this.fullName ) || alternative; + } + + toggleRemove( element:Element, value:boolean ) + { + if ( value ) + { + this.to( element, "" ); + } + else + { + this.removeFrom( element ); + } + } + + to( element:Element, value:string|number|boolean = "") + { + value = value + ""; + + if ( ! element ) + { + console.log( "Element is null", this ); + } + + if ( this._useNameSpace ) + { + element.setAttributeNS( this._nameSpace, this.fullName, value ); + + return; + } + + element.setAttribute( this.fullName, value ); + } + + replaceIn( element:Element, matcher:RegExp, replacement:string ) + { + let value = this.from( element ); + value = value.replace( matcher, replacement ); + this.to( element, value ) + } + + removeFrom( element:Element ) + { + if ( this._useNameSpace ) + { + element.removeAttributeNS( this._nameSpace, this.fullName ); + + return; + } + + element.removeAttribute( this.fullName ); + } + + copy( source:Element, target:Element ) + { + this.to( target, this.from( source ) ); + } + + +} \ No newline at end of file diff --git a/browser/dom/ElementType.ts b/browser/dom/ElementType.ts new file mode 100644 index 0000000..86571e9 --- /dev/null +++ b/browser/dom/ElementType.ts @@ -0,0 +1,172 @@ +import { DOMEditor } from "./DOMEditor"; + +export class ElementType +{ + static readonly any = new ElementType( "*" ); + static readonly input = new ElementType( "input" ); + + static readonly meta = new ElementType( "meta" ); + static readonly title = new ElementType( "title" ); + + static readonly anchor = new ElementType( "a" ); + static readonly break = new ElementType( "br" ); + + static readonly image = new ElementType( "img" ); + static readonly audio = new ElementType( "audio" ); + static readonly video = new ElementType( "video" ); + static readonly source = new ElementType( "source" ); + static readonly canvas = new ElementType( "canvas" ); + + + static readonly script = new ElementType( "script" ); + static readonly style = new ElementType( "style" ); + static readonly form = new ElementType( "form" ); + static readonly button = new ElementType( "button" ); + static readonly select = new ElementType( "select" ); + static readonly option = new ElementType( "option" ); + static readonly textArea = new ElementType( "textarea" ); + + static readonly table = new ElementType( "table" ); + static readonly thead = new ElementType( "thead" ); + static readonly tbody = new ElementType( "tbody" ); + + static readonly tr = new ElementType( "tr" ); + static readonly th = new ElementType( "th" ); + static readonly td = new ElementType( "td" ); + + static readonly hr = new ElementType( "hr" ); + + static readonly span = new ElementType( "span" ); + static readonly div = new ElementType( "div" ); + static readonly code = new ElementType( "code" ); + static readonly pre = new ElementType( "pre" ); + + static readonly p = new ElementType( "p" ); + static readonly b = new ElementType( "b" ); + static readonly i = new ElementType( "i" ); + + static readonly h1 = new ElementType( "h1" ); + static readonly h2 = new ElementType( "h2" ); + static readonly h3 = new ElementType( "h3" ); + static readonly h4 = new ElementType( "h4" ); + static readonly h5 = new ElementType( "h5" ); + static readonly h6 = new ElementType( "h6" ); + static readonly h7 = new ElementType( "h7" ); + static readonly h8 = new ElementType( "h8" ); + static readonly h9 = new ElementType( "h9" ); + static readonly h10 = new ElementType( "h10" ); + + static readonly section = new ElementType( "section" ); + + static readonly iframe = new ElementType( "iframe" ); + + static readonly svg = new ElementType( "svg" ); + + static readonly strong = new ElementType( "strong" ); + + _type:string; + _rawType:string; + _namespace:string; + + public constructor( type:string, namespace:string = undefined ) + { + this._rawType = type; + this._type = this._rawType.toUpperCase(); + + if ( namespace !== undefined ) + { + this._namespace = namespace; + } + } + + + get type() + { + return this._type; + } + + get rawType() + { + return this._rawType; + } + + get selector() + { + return this._type; + } + + forAll( target:Element|Document, callback:(element:E)=>any) + { + let elements = this.queryAll( target ); + + for ( let i = 0; i < elements.length; i++ ) + { + callback( elements[ i ] ); + } + } + + forAllInDoc( callback:(element:E)=>any) + { + this.forAll( document, callback ); + } + + queryDoc():E + { + return document.querySelector( this._rawType ) as E; + } + + query( element:Document|Element ):E + { + return element.querySelector( this._rawType ) as E; + } + + queryAll( element:Document|Element ):E[] + { + return DOMEditor.nodeListToArray( element.querySelectorAll( this._rawType ) ) as E[]; + } + + + create( children:(Node|string)|(Node|string)[] = undefined, doc:Document = undefined ):E + { + doc = doc || document; + + let anyElement = + this._namespace === undefined ? doc.createElement( this._rawType ) + : doc.createElementNS( this._namespace, this._rawType ); + + let element = anyElement as any as E; + + if ( children ) + { + if ( Array.isArray( children ) ) + { + for ( let i = 0; i < children.length; i++ ) + { + if ( typeof children[ i ] === "string" ) + { + element.appendChild( doc.createTextNode( children[ i ] as string ) ); + } + else + { + element.appendChild( children[ i ] as Node ); + } + } + } + else + { + if ( typeof children === "string" ) + { + element.appendChild( doc.createTextNode( children as string ) ); + } + else + { + element.appendChild( children as Node ); + } + } + + + } + + return element; + } +} \ No newline at end of file diff --git a/browser/expressions/AsyncBooleanExpression.ts b/browser/expressions/AsyncBooleanExpression.ts new file mode 100644 index 0000000..2bbf1cc --- /dev/null +++ b/browser/expressions/AsyncBooleanExpression.ts @@ -0,0 +1,101 @@ +export abstract class AsyncBooleanExpression +{ + abstract evaluate( t:T ):Promise; + + NOT():AsyncBooleanExpression + { + return new NotAsyncExpression( this ); + } + + AND( matcher:AsyncBooleanExpression):AsyncBooleanExpression + { + return new AndAsyncExpression( [ this, matcher ] ); + } + + OR( matcher:AsyncBooleanExpression):AsyncBooleanExpression + { + return new OrAsyncExpression( [ this, matcher ] ); + } +} + +export class NotAsyncExpression extends AsyncBooleanExpression +{ + private _matcher:AsyncBooleanExpression; + + constructor( matcher:AsyncBooleanExpression ) + { + super(); + this._matcher = matcher; + } + + async evaluate( t:T ):Promise + { + let result = await this._matcher.evaluate( t ); + return Promise.resolve( ! result ); + } + + NOT() + { + return this._matcher; + } +} + +export abstract class ListInputAsyncExpression extends AsyncBooleanExpression +{ + protected _matchers:AsyncBooleanExpression[]; + + constructor( matchers:AsyncBooleanExpression[] ) + { + super(); + this._matchers = matchers; + } +} + +export class AndAsyncExpression extends ListInputAsyncExpression +{ + async evaluate( t:T ):Promise + { + for ( let matcher of this._matchers ) + { + let result = await matcher.evaluate( t ); + + if ( ! result ) + { + return Promise.resolve( false ); + } + } + + return Promise.resolve( true ); + } + + AND( matcher:AsyncBooleanExpression ) + { + this._matchers.push( matcher ); + return this; + } +} + +export class OrAsyncExpression extends ListInputAsyncExpression +{ + async evaluate( t:T ):Promise + { + for ( let matcher of this._matchers ) + { + let result = await matcher.evaluate( t ); + + if ( result ) + { + return Promise.resolve( true ); + } + } + + return Promise.resolve( false ); + } + + OR( matcher:AsyncBooleanExpression ) + { + this._matchers.push( matcher ); + return this; + } +} + diff --git a/browser/expressions/AsyncStringMatcher.ts b/browser/expressions/AsyncStringMatcher.ts new file mode 100644 index 0000000..c8271fe --- /dev/null +++ b/browser/expressions/AsyncStringMatcher.ts @@ -0,0 +1,76 @@ +import { AsyncBooleanExpression } from "./AsyncBooleanExpression"; + +export abstract class StringValueAsyncMatcher extends AsyncBooleanExpression +{ + abstract getStringValue( t:T ):Promise; +} + +export abstract class LitaralAsyncMatcher extends StringValueAsyncMatcher +{ + _literal:L + constructor( literal:L ) + { + super(); + this._literal = literal; + } + + async evaluate( t:T ):Promise + { + + let value = await this.getStringValue( t ); + + if ( value === null ) + { + return false; + } + + return Promise.resolve( this._literal === value ); + } +} + +export abstract class ContainsLitaralAsyncMatcher extends StringValueAsyncMatcher +{ + _literal:L + constructor( literal:L ) + { + super(); + this._literal = literal; + } + + async evaluate( t:T ):Promise + { + + let value = await this.getStringValue( t ); + + if ( value === null ) + { + return false; + } + + return Promise.resolve( value.indexOf( this._literal ) !== -1 ); + } +} + +export abstract class RegExpAsyncMatcher extends StringValueAsyncMatcher +{ + private _regexp:RegExp; + + constructor( regexp:RegExp ) + { + super(); + this._regexp = regexp; + } + + async evaluate( t:T ):Promise + { + let value = await this.getStringValue( t ); + + if ( value === null ) + { + return false; + } + + return Promise.resolve( this._regexp.test( value ) ); + } + +} \ No newline at end of file diff --git a/browser/expressions/BooleanExpression.ts b/browser/expressions/BooleanExpression.ts new file mode 100644 index 0000000..f4bd4fd --- /dev/null +++ b/browser/expressions/BooleanExpression.ts @@ -0,0 +1,114 @@ +export abstract class BooleanExpression +{ + abstract evaluate( t:T ):boolean; + + NOT():BooleanExpression + { + return new NotExpression( this ); + } + + AND( matcher:BooleanExpression):BooleanExpression + { + return new AndExpression( [ this, matcher ] ); + } + + OR( matcher:BooleanExpression):BooleanExpression + { + return new OrExpression( [ this, matcher ] ); + } + + AND_ALL( matchers:BooleanExpression[] ):BooleanExpression + { + if ( matchers.length === 0 ) + { + return this; + } + + let outputExpression = this.AND( matchers[ 0 ] ); + + for ( let i = 1; i < matchers.length; i++ ) + { + outputExpression = outputExpression.AND( matchers[ i ] ); + } + + return outputExpression; + } + + OR_ANY( matchers:BooleanExpression[] ):BooleanExpression + { + if ( matchers.length === 0 ) + { + return this; + } + + let outputExpression = this.OR( matchers[ 0 ] ); + + for ( let i = 1; i < matchers.length; i++ ) + { + outputExpression = outputExpression.OR( matchers[ i ] ); + } + + return outputExpression; + } +} + +export class NotExpression extends BooleanExpression +{ + private _matcher:BooleanExpression; + + constructor( matcher:BooleanExpression ) + { + super(); + this._matcher = matcher; + } + + evaluate( t:T ) + { + return ! this._matcher.evaluate( t ); + } + + NOT() + { + return this._matcher; + } +} + +export abstract class ListInputExpression extends BooleanExpression +{ + protected _matchers:BooleanExpression[]; + + constructor( matchers:BooleanExpression[] ) + { + super(); + this._matchers = matchers; + } +} + +export class AndExpression extends ListInputExpression +{ + evaluate( t:T ) + { + return this._matchers.every( m => m.evaluate( t ) ); + } + + AND( matcher:BooleanExpression ) + { + this._matchers.push( matcher ); + return this; + } +} + +export class OrExpression extends ListInputExpression +{ + evaluate( t:T ) + { + return this._matchers.some( m => m.evaluate( t ) ); + } + + OR( matcher:BooleanExpression ) + { + this._matchers.push( matcher ); + return this; + } +} + diff --git a/browser/expressions/StringMatcher.ts b/browser/expressions/StringMatcher.ts new file mode 100644 index 0000000..187d26f --- /dev/null +++ b/browser/expressions/StringMatcher.ts @@ -0,0 +1,40 @@ +import { BooleanExpression } from "./BooleanExpression"; + +export abstract class StringValueMatcher extends BooleanExpression +{ + abstract getStringValue( t:T ):string; +} + +export abstract class LitaralMatcher extends StringValueMatcher +{ + _literal:L + constructor( literal:L ) + { + super(); + this._literal = literal; + } + + evaluate( t:T ) + { + let value = this.getStringValue( t ); + return this._literal === value; + } +} + +export abstract class RegExpMatcher extends StringValueMatcher +{ + private _regexp:RegExp; + + constructor( regexp:RegExp ) + { + super(); + this._regexp = regexp; + } + + evaluate( t:T ) + { + let value = this.getStringValue( t ); + return this._regexp.test( value ); + } + +} \ No newline at end of file diff --git a/browser/geometry/Range.ts b/browser/geometry/Range.ts new file mode 100644 index 0000000..eed2f6c --- /dev/null +++ b/browser/geometry/Range.ts @@ -0,0 +1,115 @@ +import { MathX } from "../math/MathX"; + +export class Range +{ + min:number; + max:number; + + constructor( min:number, max:number ) + { + this.min = min; + this.max = max || min; + } + + static from( values:number[] ) + { + return new Range( values[ 0 ], values[ 1 ] ); + } + + ensurecorrectness():void + { + if ( this.max < this.min ) + { + let b = this.max; + this.max = this.min; + this.min = b; + } + } + + contains( value:number ):boolean + { + return this.min <= value && value <= this.max; + } + + overlaps( other:Range ):boolean + { + if ( other.max < this.min ) { return false; } + if ( other.min > this.max ) { return false; } + + if ( other.contains( this.min ) || other.contains( this.max ) ) + { + return true; + } + + if ( this.contains( this.min ) || this.contains( this.max ) ) + { + return true; + } + + return false; + } + + getOverlap( other:Range ) + { + if ( ! this.overlaps( other ) ) + { + return null; + } + + return new Range( Math.max( this.min, other.min ), Math.min( this.max, other.max ) ); + } + + get center():number { return 0.5 * ( this.max + this.min ); } + get length():number { return this.max - this.min; } + + distanceTo( other:Range ):number + { + let center = this.center; + let othercenter = other.center; + + let ownLength = this.length; + let otherLength = other.length; + + let distance = Math.abs( center - othercenter ); + + return Math.max( 0, distance - 0.5 * ( ownLength + otherLength ) ); + + } + + copy():Range + { + return new Range( this.min, this.max ); + } + + sampleAt( normalized:number ):number + { + return this.min + ( this.max - this.min ) * normalized; + } + + normalize( value:number ):number + { + return ( value - this.min ) / ( this.max - this.min ); + } + + clamp( value:number ) + { + return MathX.clamp( value, this.min, this.max ); + } + + equals( range:Range ) + { + return range.min === this.min && range.max === this.max; + } + + static get of_01():Range + { + return new Range( 0, 1 ); + } + + static get Maximum():Range + { + return new Range( -Number.MAX_VALUE, Number.MAX_VALUE ); + } + + +} \ No newline at end of file diff --git a/browser/graphs/ArrayChildrenTreeWalker.ts b/browser/graphs/ArrayChildrenTreeWalker.ts new file mode 100644 index 0000000..9ca6544 --- /dev/null +++ b/browser/graphs/ArrayChildrenTreeWalker.ts @@ -0,0 +1,48 @@ +import { TreeWalker } from './TreeWalker'; + +export abstract class ArrayChildrenTreeWalker extends TreeWalker +{ + _parentMap = new Map(); + + abstract getChildren( t:T ):T[]; + + clearParentMap() + { + this._parentMap.clear(); + } + + updateParentMap( root:T, ...otherRoots:T[] ) + { + let stack = [ root ].concat( otherRoots ); + + while ( stack.length > 0 ) + { + let parent = stack.shift(); + let children = this.getChildren( parent ); + + for ( let child of children ) + { + this._parentMap.set( child, parent ); + stack.push( child ); + } + + } + } + + childAt( t:T, index:number ) + { + return this.getChildren( t )[ index ]; + } + + parent( t:T ):T + { + return this._parentMap.get( t ); + } + + numChildren( t:T ) + { + return this.getChildren( t ).length; + } + + +} \ No newline at end of file diff --git a/browser/graphs/HTMLNodeTreeWalker.ts b/browser/graphs/HTMLNodeTreeWalker.ts new file mode 100644 index 0000000..6b9c052 --- /dev/null +++ b/browser/graphs/HTMLNodeTreeWalker.ts @@ -0,0 +1,42 @@ +import { TreeWalker } from './TreeWalker'; + +export class HTMLNodeTreeWalker extends TreeWalker +{ + private static _instance:HTMLNodeTreeWalker; + + static get $() + { + if ( HTMLNodeTreeWalker._instance ) + { + return HTMLNodeTreeWalker._instance; + } + + HTMLNodeTreeWalker._instance = new HTMLNodeTreeWalker(); + return HTMLNodeTreeWalker._instance; + } + + parent( node:Node ) + { + return node.parentNode; + } + + childAt( node:Node, index:number ) + { + if ( Node.ELEMENT_NODE !== node.nodeType ) + { + return null; + } + + return ( node as Element ).childNodes[ index ]; + } + + numChildren( node:Node ):number + { + if ( Node.ELEMENT_NODE !== node.nodeType ) + { + return 0; + } + + return ( node as Element ).childNodes.length; + } +} \ No newline at end of file diff --git a/browser/graphs/TreeWalker.ts b/browser/graphs/TreeWalker.ts new file mode 100644 index 0000000..ffe0982 --- /dev/null +++ b/browser/graphs/TreeWalker.ts @@ -0,0 +1,266 @@ + +// (boolean|N|number)\s+(\w+)\(\s*(?:(\w+)\s*(\w+))\s*\) +// $2( $4:$3 ):$1 +// (boolean|N|number)\s+(\w+)\(\s*(?:(\w+)\s*(\w+))\s*(?:\,\s*(\w+)\s*(\w+))\s*\) +// $2( $4:$3, $6:$5 ):$1 +// (\w+)\(\s+(\w+)\s+\) +// this.$1( $2 ) +// (\w+)\(\s+(\w+)\s+\) +// this.$1( $2 ) +export abstract class TreeWalker +{ + abstract parent( node:N ):N; + + abstract childAt( node:N, index:number ):N; + + abstract numChildren( node:N ):number; + + hasChildren( node:N ):boolean + { + return this.numChildren( node )>0; + } + + hasParent( node:N ):boolean + { + return this.parent( node ) != null; + } + + childIndexOf( node:N ):number + { + let p = this.parent( node ); + + if ( p == null ) + { + return -1; + } + + let numKids = this.numChildren( p ); + + for ( let i = 0; i= this.numChildren( p ) ) + { return null; } + + return this.childAt( p, index ); + } + + + nextSibling( node:N ):N + { + let index = this.childIndexOf( node ); + return this.siblingAt( node, index+1 ); + } + + + previousSibling( node:N ):N + { + let index = this.childIndexOf( node ); + return this.siblingAt( node, index-1 ); + } + + + hasSiblingAt( node:N, index:number ):boolean + { + let p = this.parent( node ); + if ( p == null || index<0 || index >= this.numChildren( p ) ) + { return false; } + return true; + } + + + firstChild( node:N ):N + { + return this.numChildren( node )<=0?null:this.childAt( node, 0 ); + } + + + lastChild( node:N ):N + { + let num = this.numChildren( node ); + return num <= 0?null:this.childAt( node, num-1 ); + } + + forAll( node:N, callback:( node:N ) => void ) + { + let end = this.iterationEndOf( node ); + + let it = node; + + while ( it && it !== end ) + { + callback( it ); + it = this.nextNode( it ); + } + + } + + nextNode( node:N ):N + { + if ( this.hasChildren( node ) ) + { + return this.firstChild( node ); + } + + let next = this.nextSibling( node ); + + if ( next != null ) + { + return next; + } + + let parent = this.parent( node ); + + while ( parent != null ) + { + let n = this.nextSibling( parent ); + + if ( n != null ) + { + return n; + } + + parent = this.parent( parent ); + } + + return null; + } + + + previousNode( node:N ):N + { + let prev = this.previousSibling( node ); + + if ( prev != null ) + { + while ( this.hasChildren( prev ) ) + { + prev = this.lastChild( prev ); + } + return prev; + } + + return this.parent( node ); + } + + + rootParent( node:N ):N + { + node = this.parent( node ); + + if ( node == null ) + { + return null; + } + + while ( this.hasParent( node ) ) + { + node = this.parent( node ); + } + + return node; + } + + + lastGrandChild( node:N ):N + { + if ( this.hasChildren( node ) ) + { + node = this.lastChild( node ); + + while ( this.hasChildren( node ) ) + { + node = this.lastChild( node ); + } + + return node; + } + + return null; + } + + isChildOfAny( child:N, set:Set ):boolean + { + let p = this.parent( child ); + + while ( p ) + { + if ( set.has( p ) ) + { + return true; + } + + p = this.parent( p ); + } + + return false; + } + + childOf( child:N, parent:N ):boolean + { + let p = this.parent( child ); + + while ( p != null ) + { + if ( p == parent ) + { + return true; + } + p = this.parent( p ); + } + + return false; + } + + + numParents( node:N ):number + { + let num = 0; + let p = this.parent( node ); + + while ( p != null ) + { + num++; + p = this.parent( p ); + } + + return num; + } + + + lastOuterNode( node:N ):N + { + while ( this.hasChildren( node ) ) + { + node = this.lastChild( node ); + } + + return node; + } + + + nextNonChild( node:N ):N + { + return this.nextNode( this.lastOuterNode( node ) ); + } + + iterationEndOf( node:N ):N + { + return this.nextNonChild( node ); + } + +} + diff --git a/browser/math/MathX.ts b/browser/math/MathX.ts new file mode 100644 index 0000000..9c4de6c --- /dev/null +++ b/browser/math/MathX.ts @@ -0,0 +1,283 @@ +export class MathX +{ + static stringify( value:number, fractionDigits:number = 3, intDigits:number = 0, cutZeroFraction:boolean = true ) + { + let fixed = value.toFixed( fractionDigits ); + + let regex = /(\+|\-)?(\d*)\.(\d*)/; + let parsed = regex.exec( fixed ); + + let sign = parsed[ 1 ] || ""; + let ints = parsed[ 2 ]; + let fraction = parsed[ 3 ]; + + while ( intDigits > ints.length ) + { + ints = "0" + ints; + } + + if ( cutZeroFraction ) + { + fraction = fraction.replace( /0+$/, "" ); + } + + if ( fraction.length === 0 ) + { + return sign + ints; + } + + return sign + ints + "." + fraction; + + } + + static log( value:number, base:number = Math.E ) + { + return Math.log( value )/ Math.log( base ); + } + + static normalize( value:number, min:number, max:number ) + { + return ( value - min ) / ( max - min ); + } + + static map( value:number, inputMin:number, inputMax:number, outputMin:number, outputMax:number ) + { + let normalized = MathX.normalize( value, inputMin, inputMax ); + return MathX.lerp( outputMin, outputMax, normalized ); + } + + static mapClamped( value:number, inputMin:number, inputMax:number, outputMin:number, outputMax:number ) + { + let normalized = MathX.normalize( value, inputMin, inputMax ); + normalized = MathX.clamp01( normalized ); + return MathX.lerp( outputMin, outputMax, normalized ); + } + + static maxAbs( ...values:number[] ) + { + let maxValue = 0; + let index = 0; + + for ( let i = 0; i < values.length; i++ ) + { + let v = values[ i ]; + let abs = Math.abs( v ); + + if ( Math.abs( v ) > maxValue ) + { + maxValue = abs; + index = i; + } + } + + return values[ index ]; + } + + + static median( ...values:number[] ) + { + let sorted = [].concat( values ).sort( ( a,b ) => a - b ); + + console.log( sorted ); + return sorted[ Math.round( sorted.length/2 ) ]; + } + + static average( ...values:number[] ) + { + let value = 0; + + values.forEach( v => value += v ) + + return value / values.length; + } + + static base( exponent:number, power:number ) + { + return Math.pow( power, 1 / exponent ); + } + + static exponent( base:number, power:number ) + { + return Math.log( power ) / Math.log( base ); + } + + + static format( value:number, numZeros:number = 2 ):string + { + if ( numZeros <= 0 ) + { + return Math.round( value ) + ""; + } + + numZeros = Math.round( numZeros ); + + var sign = value < 0 ? "-" : ""; + value = Math.abs( value ); + + var roundedBiggerValue = Math.round( value * Math.pow( 10, numZeros ) ); + var stringValue = roundedBiggerValue + ""; + var minimumLength = numZeros + 1; + + while ( stringValue.length < minimumLength ) + { + stringValue = "0" + stringValue; + } + + + var split = stringValue.length - numZeros; + + return sign + stringValue.substring( 0, split ) + "." + stringValue.substring( split ); + } + + static lerpAngle( a:number, b:number, amount:number ) + { + let difference = MathX.angleDifference( a, b ); + + + let result = MathX.repeat( a + difference * amount, 360 ); + console.log( "lerping angle", a, b, "diff:", difference, "amount:", amount, ">>", result ) + return result; + } + + static modulo( a:number, b:number ) + { + return a - Math.floor( a / b ) * b; + } + + static angleDifference( a:number, b:number ) + { + a = MathX.repeat( a, 360 ); + b = MathX.repeat( b, 360 ); + + let difference = b - a; + + if ( difference < -180 ) + { + difference = 360 + difference; + return difference; + } + + if ( difference > 180 ) + { + difference = difference - 360; + return difference; + } + + return difference; + } + + static align( baseWidth:number, elementWidth:number, alginmentNormalized:number ) + { + return ( baseWidth - elementWidth ) * alginmentNormalized; + } + + static clamp( value:number, min:number, max:number ) + { + return value < min ? min : value > max ? max : value; + } + + static clamp01( value:number ) + { + return value < 0 ? 0 : value > 1 ? 1 : value; + } + + static repeat( value:number, range:number ) + { + while ( value < 0 ) + { + value += range; + } + + while ( value >= range ) + { + value -= range; + } + + return value; + } + + static repeatPolar( value:number, max:number ) + { + while ( value > max ) + { + value -= max * 2; + } + + while ( value < -max ) + { + value += max * 2; + } + + return value; + + } + + static lerp( a:number, b:number, t:number ) + { + t = MathX.clamp01( t ); + return a + t * ( b - a ); + } + + static lerpUnclamped( a:number, b:number, t:number ) + { + return a + t * ( b - a ); + } + + static triangle( normalized:number ) + { + normalized = normalized * 2; + let invertedNormalized = 2 - normalized; + + return MathX.clamp01( Math.min( normalized, invertedNormalized ) ); + } + + static quantizeFloored( value:number, quantization:number ) + { + return Math.floor( value / quantization ) * quantization; + } + + static quantizeCeiled( value:number, quantization:number ) + { + return Math.ceil( value / quantization ) * quantization; + } + + static quantizeRounded( value:number, quantization:number ) + { + return Math.round( value / quantization ) * quantization; + } + + static inRange( value:number, lower:number, higher:number ) + { + if ( value < lower ) + { + return false; + } + + if ( value > higher ) + { + return false; + } + + return true; + } + + static advanceIndex( array:T[], currentIndex:number, direction:number ) + { + let nextIndex = currentIndex + direction; + nextIndex = MathX.repeat( nextIndex, array.length ); + + return nextIndex; + } + + static nextIndex( array:T[], currentIndex:number ) + { + return MathX.advanceIndex( array, currentIndex, 1 ); + } + + static previousIndex( array:T[], currentIndex:number ) + { + return MathX.advanceIndex( array, currentIndex, -1 ); + } + + +} diff --git a/browser/random/IncrementalIDGenerator.ts b/browser/random/IncrementalIDGenerator.ts new file mode 100644 index 0000000..b18f1c8 --- /dev/null +++ b/browser/random/IncrementalIDGenerator.ts @@ -0,0 +1,68 @@ +import { RandomEngine } from "./RandomEngine"; +import { JSRandomEngine } from "./JSRandomEngine"; + +export class IncrementalIDGenerator +{ + static readonly LETTERS_CHARACTER_SET = + "ABCDEFGHIJKLMNOPQRSTUVWYXZabcdefghijklmnopqrstuvwyz"; + + static readonly BASE_62_CHARACTER_SET = + "ABCDEFGHIJKLMNOPQRSTUVWYXZabcdefghijklmnopqrstuvwyz0123456789"; + + static readonly BASE_64_URL_CHARACTER_SET = + "ABCDEFGHIJKLMNOPQRSTUVWYXZabcdefghijklmnopqrstuvwyz0123456789-_"; + + static readonly BASE_64_CHARACTER_SET = + "ABCDEFGHIJKLMNOPQRSTUVWYXZabcdefghijklmnopqrstuvwyz0123456789+/"; + + private _set = IncrementalIDGenerator.BASE_64_URL_CHARACTER_SET; + private _state:number[] = []; + + constructor( set:string = null ) + { + this._set = set || IncrementalIDGenerator.BASE_64_URL_CHARACTER_SET; + } + + setState( state:number[] ) + { + this._state = state; + } + + private _incrementAt( position:number ) + { + if ( position >= this._state.length ) + { + this._state.push( 0 ); + } + else if ( this._state[ position ] === ( this._set.length - 1 ) ) + { + this._state[ position ] = 0; + this._incrementAt( position + 1 ); + } + else + { + this._state[ position ] ++; + } + } + + private _getID() + { + let id = ""; + + for ( let i = 0; i < this._state.length; i++ ) + { + id += this._set[ this._state[ i ] ]; + } + + return id; + } + + createID() + { + this._incrementAt( 0 ); + return this._getID(); + } + + + +} \ No newline at end of file diff --git a/browser/random/JSRandomEngine.ts b/browser/random/JSRandomEngine.ts new file mode 100644 index 0000000..a3bc54e --- /dev/null +++ b/browser/random/JSRandomEngine.ts @@ -0,0 +1,14 @@ +import { RandomEngine } from './RandomEngine'; +export class JSRandomEngine extends RandomEngine +{ + public next() + { + return Math.random(); + } + + private static _instance = new JSRandomEngine(); + static get $() + { + return JSRandomEngine._instance; + } +} \ No newline at end of file diff --git a/browser/random/LCG.ts b/browser/random/LCG.ts new file mode 100644 index 0000000..c8afb03 --- /dev/null +++ b/browser/random/LCG.ts @@ -0,0 +1,73 @@ +import { SeedableRandomEngine } from "./RandomEngine"; + +export class LCGState +{ + public modulus:number; + public multiplier:number; + public increment:number; + public state:number; +} + +export class LCG extends SeedableRandomEngine +{ + private _seedState:LCGState = new LCGState(); + private _normalizer:number; + + constructor() + { + super(); + this.setParameters( Math.pow( 2, 32 ), 1664525, 1013904223 ); + } + + setParameters( modulus:number, multiplier:number, increment:number ):void + { + this._seedState.modulus = modulus; + this._seedState.multiplier = multiplier; + this._seedState.increment = increment; + this._seedState.state = 0; + this._normalizer = 1 / ( this._seedState.modulus ); + } + + getSeedState() + { + var seedState = new LCGState(); + + seedState.modulus = this._seedState.modulus; + seedState.multiplier = this._seedState.multiplier; + seedState.increment = this._seedState.increment; + seedState.state = this._seedState.state; + + return seedState; + } + + setSeedState( seedState:LCGState ) + { + this._seedState.modulus = seedState.modulus; + this._seedState.multiplier = seedState.multiplier; + this._seedState.increment = seedState.increment; + this._seedState.state = seedState.state; + this._normalizer = 1 / ( this._seedState.modulus ); + } + + next() + { + this._seedState.state = ( this._seedState.state * this._seedState.multiplier + + this._seedState.increment) % this._seedState.modulus; + var value = this._seedState.state * this._normalizer; + return Math.abs( value ); + } + + setSeed( seed:number ) + { + this._seedState.state = seed % this._seedState.modulus; + } + + warmUp( runs:number = 1000 ) + { + for ( let i = 0; i < runs; i++) + { + this.next(); + } + } + +} \ No newline at end of file diff --git a/browser/random/RandomEngine.ts b/browser/random/RandomEngine.ts new file mode 100644 index 0000000..8f842c9 --- /dev/null +++ b/browser/random/RandomEngine.ts @@ -0,0 +1,212 @@ +import { HTMLColor } from '../colors/HTMLColor'; +import { HSLColor } from '../colors/HSLColor'; +import { RGBColor } from "../colors/RGBColor"; +import { Range } from '../geometry/Range'; + + +export abstract class RandomEngine +{ + abstract next():number; + + value( scalar:number ):number + { + return this.next() * scalar; + } + + range( a:number, b:number ):number + { + return this.next()*( b - a ) + a; + } + + in( range:Range ):number + { + return this.range( range.min, range.max ); + } + + polarOne():number + { + return this.next() * 2 - 1; + } + + polar( value:number ):number + { + return this.polarOne() * value; + } + + bool():boolean + { + return this.next() > 0.5; + } + + percentageVariation( variation:number ):Range + { + var value = ( 100 - variation ) / 100; + return new Range( value, 1 / value ); + } + + withChanceOf( value:number ):boolean + { + return ( this.next() * 100 ) <= value ; + } + + + randomHue( saturation:number = 1, luminance:number = 0.5):HTMLColor + { + let hue = this.range( 0, 360 ); + let color = new HSLColor( hue, saturation, luminance ); + return color; + } + + lerpRGB( colors:HTMLColor[]):HTMLColor + { + if ( colors.length == 1 ) + { + return colors[ 0 ]; + } + + return RGBColor.lerpFrom( colors, this.next() ); + } + + /* + inCubeOne():Vector3 + { + return new Vector3( this.polarOne(), this.polarOne(), this.polarOne() ); + } + + inCube( size:number ):Vector3 + { + return this.inCubeOne().multiplyScalar( size * 0.5 ); + } + + inSphereOne():Vector3 + { + var inCube = this.inCubeOne(); + + if ( inCube.lengthSq() > 1 ) + { + inCube.normalize().multiplyScalar( this.next() ); + } + + return inCube; + } + + inSphere( size:number ):Vector3 + { + return this.inSphereOne().multiplyScalar( size * 0.5 ); + } + + */ + + integerInclusive( min:number, max:number ):number + { + return ( Math.floor( this.next() * ( max - min + 1 ) ) + min ) ; + } + + integerInclusiveFromZero( max:number ):number + { + return this.integerInclusive( 0, max ); + } + + + integerExclusive( min:number, max:number ):number + { + var nextValue = this.next(); + var randomValue = nextValue * ( max - min ) + min; + + var value = ( Math.floor( randomValue ) ); + return Math.min( max - 1, value ); + } + + integerExclusiveFromZero( max:number ):number + { + return this.integerExclusive( 0, max ); + } + + + fromString( text: string, alternative:string = null ):string + { + if ( ! text || text.length === 0 ) + { + return alternative; + } + + let index = this.integerExclusive( 0, text.length ); + return text[ index ]; + } + + from( array: T[], alternative:T = null ):T + { + if ( array.length == 0 ) + { + return alternative; + } + + var index = this.integerExclusive( 0, array.length ); + return array[ index ]; + } + + arrayFrom( numElements:number, array: T[], alternative:T = null ):T[] + { + let randomArray:T[] = []; + + for ( let i = 0; i < numElements; i++ ) + { + randomArray.push( this.from( array, alternative ) ); + } + + return randomArray; + } + + fromValues( first:T, ...values:T[] ):T + { + if ( values.length == 0 ) + { + return first; + } + + var index = this.integerExclusive( 0, values.length + 1 ); + + return index == 0 ? first : values[ index - 1]; + } + + arrayFromValues( numElements:number, first:T, ...values:T[] ):T[] + { + let randomArray:T[] = []; + + for ( let i = 0; i < numElements; i++ ) + { + randomArray.push( this.from( [first].concat( values ) ) ); + } + + return randomArray; + } + + fromSet( set:Set ) + { + let index = this.integerExclusiveFromZero( set.size ); + let i = 0; + + for ( let t of set ) + { + if ( index === i ) + { + return t; + } + + i++; + } + + return null; + } + + +} + +export abstract class SeedableRandomEngine extends RandomEngine +{ + abstract setSeed( seed:number ):void; + abstract getSeedState():S; + abstract setSeedState( seedState:S ):void; + +} + diff --git a/browser/random/RandomUIDGenerator.ts b/browser/random/RandomUIDGenerator.ts new file mode 100644 index 0000000..9b1c04a --- /dev/null +++ b/browser/random/RandomUIDGenerator.ts @@ -0,0 +1,54 @@ +import { RandomEngine } from "./RandomEngine"; +import { JSRandomEngine } from "./JSRandomEngine"; + +export class RandomUIDGenerator +{ + static readonly UID_CHARACTER_SET:string = + "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; + + static readonly UID_NUM_CHARACTERS:number = 16; + + private _characterSet:string = RandomUIDGenerator.UID_CHARACTER_SET; + get characterSet(){ return this._characterSet; } + + private _numCharacters:number = RandomUIDGenerator.UID_NUM_CHARACTERS; + get numCharacters(){ return this._numCharacters; } + + set( characterSet:string, numCharacters:number ) + { + this._characterSet = characterSet; + this._numCharacters = numCharacters; + + return this; + } + + + + static createUID( randomEngine:RandomEngine = null ) + { + let id = ""; + randomEngine = randomEngine ||JSRandomEngine.$; + + for ( let i = 0; i < RandomUIDGenerator.UID_NUM_CHARACTERS; i++ ) + { + let randomCharacter = randomEngine.fromString( RandomUIDGenerator.UID_CHARACTER_SET ); + id += randomCharacter; + } + + return id; + } + + generate( randomEngine:RandomEngine = null ) + { + let id = ""; + randomEngine = randomEngine || JSRandomEngine.$; + + for ( let i = 0; i < this._numCharacters; i++ ) + { + let randomCharacter = randomEngine.fromString( this._characterSet ); + id += randomCharacter; + } + + return id; + } +} \ No newline at end of file diff --git a/browser/random/SeedGenerator.ts b/browser/random/SeedGenerator.ts new file mode 100644 index 0000000..1217241 --- /dev/null +++ b/browser/random/SeedGenerator.ts @@ -0,0 +1,35 @@ +import { JSRandomEngine } from "./JSRandomEngine"; +import { LCG } from "./LCG"; +import { RandomEngine, SeedableRandomEngine } from "./RandomEngine"; + +export class SeedGenerator +{ + static fromText( source:string, randomEngine:SeedableRandomEngine = null ) + { + if ( ! randomEngine ) + { + randomEngine = new LCG(); + } + + randomEngine.setSeed( 18593 ); + + let seed = 0; + + for ( let i = 0; i < source.length; i++ ) + { + let characterValue = source.charCodeAt( i ); + + seed += characterValue * 767417 + 13 + randomEngine.next() * characterValue; + + if ( seed > 32000 ) + { + seed = seed % 32000; + } + } + + console.log( "SEED FROM TEXT:", source, seed ) + + return seed; + + } +} \ No newline at end of file diff --git a/browser/templates/ElementProcessor.ts b/browser/templates/ElementProcessor.ts new file mode 100644 index 0000000..0d14179 --- /dev/null +++ b/browser/templates/ElementProcessor.ts @@ -0,0 +1,7 @@ +export abstract class ElementProcessor +{ + abstract getElementName():string; + abstract processElement( element:Element ):Element; + hasPostProcessor(){ return false;} + postProcessElement( inputElement:Element, processedElement:Element ){} +} \ No newline at end of file diff --git a/browser/templates/TemplateReplacer.ts b/browser/templates/TemplateReplacer.ts new file mode 100644 index 0000000..56dda52 --- /dev/null +++ b/browser/templates/TemplateReplacer.ts @@ -0,0 +1,406 @@ + +import { HTMLNodeTreeWalker } from "../graphs/HTMLNodeTreeWalker"; +import { DOMEditor as HTMLEditor } from "../dom/DOMEditor"; +import { TemplatesSourceLexer } from "./TemplatesSourceLexer"; +import { TemplateSourceMatchers } from "./TemplateSourceMatchers"; +import { ElementAttribute } from "../dom/ElementAttribute"; + + + +export class TemplateReplacer +{ + static readonly dataAttributeAssignmentPrefix = "data-@"; + static readonly unwrapTemplate = new ElementAttribute( "-unwrap--template" ); + + + replace( target:Element, classElement:Element ) + { + let replacingElement = target.ownerDocument.importNode( classElement, true ) as Element; + + this._processElement( target, replacingElement ); + + if ( target.parentElement ) + { + target.parentElement.replaceChild( replacingElement, target ); + } + + + let walker = new HTMLNodeTreeWalker(); + walker.forAll( replacingElement, e => this._processAttributes( target, e as Element ) ); + + try + { + HTMLEditor.copyAllAttributes( target, replacingElement ); + } + catch( e ) + { + replacingElement = HTMLEditor.copyAllAttributesThroughRawHTML( target, replacingElement ); + } + + return replacingElement; + } + + replaceChildren( target:Element, replacement:Element ) + { + //console.log( "REPLACING CHILDREN", target.nodeName, ">>", replacement.nodeName ); + this._processElement( target, replacement ); + } + + private _processAttributes( target:Element, element:Element ) + { + if ( ! element.attributes ) + { + return; + } + + let attributes = []; + + + for ( let i = 0; i < element.attributes.length; i++ ) + { + attributes.push( element.attributes[ i ] ); + } + + for ( let i = 0; i < attributes.length; i++ ) + { + let attributeName = attributes[ i ].nodeName; + + + if ( ! attributeName.startsWith( TemplateReplacer.dataAttributeAssignmentPrefix ) ) + { + //console.log( "NOT ASSIGNMENT:", attributeName ); + continue; + } + else + { + //console.log( "IS ASSIGNMENT:", attributeName ); + } + + let name = attributeName.substring( TemplateReplacer.dataAttributeAssignmentPrefix.length ); + element.removeAttribute( attributeName ); + + TemplateSourceMatchers.regex.lastIndex = 0; + + let match = TemplateSourceMatchers.regex.exec( attributes[ i ].nodeValue ); + + if ( ! match ) + { + + let nodeValueSnippet = "(...too much data...)"; + + if ( attributes[ i ].nodeValue.length < 50 ) + { + nodeValueSnippet = attributes[ i ].nodeValue; + } + else if ( attributes[ i ].nodeValue.length < 250 ) + { + nodeValueSnippet = attributes[ i ].nodeValue.substring( 0, 50 ); + } + + + + // console.log( "NOTHING FOR ASSIGNMENT:", match, name, attributeName, nodeValueSnippet ); + continue; + } + + + let lexer = new TemplatesSourceLexer(); + let outputValue:string[] = []; + let nodeValueBefore = attributes[ i ].nodeValue; + + // outputValue = this._processTemplateSourceText( target, nodeValueBefore ); + + let lexerEvents = lexer.lexToList( nodeValueBefore ); + + let defaultValue = null; + + for ( let i = 0; i < lexerEvents.length; i++ ) + { + let event = lexerEvents[ i ]; + let subMatchValue = event.getMatch( nodeValueBefore ); + + + if ( event.isError ) + { + console.log( `Error lexing attribute ${nodeValueBefore} at position ${event.offset}` ); + return; + } + + if ( event.isDone ) + { + continue; + } + + if ( TemplatesSourceLexer.DEFAULT === event.type ) + { + defaultValue = TemplatesSourceLexer.defaultMatcher.getMatch( subMatchValue, 1 ); + continue; + } + + + if ( TemplatesSourceLexer.CONTENT === event.type ) + { + outputValue.push( subMatchValue ); + } + else + { + TemplateSourceMatchers.regex.lastIndex = 0; + let subMatch = TemplateSourceMatchers.regex.exec( subMatchValue ); + let templateSource = TemplateSourceMatchers.createTemplateSource( subMatch ); + + if ( defaultValue ) + { + templateSource.defaultValue = defaultValue; + } + + let sourceValue = templateSource.getInnerHTML( target ); + sourceValue = sourceValue.replace( "&&", "&" ); + outputValue.push( sourceValue ); + } + + defaultValue = null; + + } + + + + let newNodeValue = outputValue.join( "" ); + + if ( newNodeValue !== "" ) + { + element.setAttribute( name, newNodeValue ); + //console.log( "ASSIGNING ", name, newNodeValue ); + } + else + { + //console.log( "NOT ASSIGNING (VALUE INVALID): ", name, "(" + attributeName + ")", lexerEvents ); + } + + /* + let elementSource = this._createElementSource( match ); + let sourceValue = elementSource.getInnerHTML( target ); + + let setAttributeValue = true; + + if ( sourceValue === "" ) + { + setAttributeValue = false; + } + + if ( setAttributeValue ) + { + element.setAttribute( name, sourceValue ); + //console.log( "setAttribute", name, attributeName, elementSource ); + }*/ + + + + } + } + + + + + private _processElement( target:Element, childElement:Element ) + { + var children = []; + + for ( let i = 0; i < childElement.childNodes.length; i++ ) + { + let child = childElement.childNodes[ i ]; + children.push( child ); + } + + for ( let i = 0; i < children.length; i++ ) + { + let child = children[ i ]; + + if ( Node.TEXT_NODE == child.nodeType ) + { + this._processTextNode( target, child ); + } + else if ( Node.ELEMENT_NODE == child.nodeType ) + { + this._processElement( target, child as Element); + } + } + + } + + + + private _processTextNodeOld( target:Element, textNode:Node ) + { + let text = textNode.nodeValue; + + + TemplateSourceMatchers.regex.lastIndex = 0; + + let match = TemplateSourceMatchers.regex.exec( text ); + + //console.log( "Matching", `"${text}"`, match ); + + if ( ! match ) + { + return; + } + + let lastIndex = 0; + let nodes:Node[] = []; + + while ( match ) + { + let matchIndex = match.index; + + let range = matchIndex - lastIndex; + + if ( range > 0 ) + { + let textRange = text.substring( lastIndex, matchIndex ); + let rangeNode = document.createTextNode( textRange ) + nodes.push( rangeNode ); + } + + let templateSource = TemplateSourceMatchers.createTemplateSource( match ); + let targetNodes = templateSource.getNodes( target ); + targetNodes.forEach( n => nodes.push( n ) ); + + lastIndex = matchIndex + match[ 0 ].length; + + match = TemplateSourceMatchers.regex.exec( text ); + + if ( match ) + { + continue; + } + + range = text.length - lastIndex; + + if ( range > 0 ) + { + let textRange = text.substring( lastIndex, text.length ); + let rangeNode = document.createTextNode( textRange ) + nodes.push( rangeNode ); + } + } + + for ( let i = 0; i < nodes.length; i++ ) + { + textNode.parentElement.insertBefore( nodes[ i ], textNode ); + } + + textNode.parentElement.removeChild( textNode ); + } + + private _processTextNode( target:Element, textNode:Node ) + { + let text = textNode.nodeValue; + + let nodes = this._processTemplateSourceText( target, text ); + + //let nodes = outputValue.map( o => document.createTextNode( o ) ); + + for ( let i = 0; i < nodes.length; i++ ) + { + textNode.parentElement.insertBefore( nodes[ i ], textNode ); + } + + textNode.parentElement.removeChild( textNode ); + } + + _processTemplateSourceText( target:Element, text:string ):Node[] + { + let lexer = new TemplatesSourceLexer(); + let lexerEvents = lexer.lexToList( text ); + + let outputs:Node[] = []; + + let defaultValue = null; + + for ( let i = 0; i < lexerEvents.length; i++ ) + { + let event = lexerEvents[ i ]; + let subMatchValue = event.getMatch( text ); + + if ( event.isError ) + { + console.log( `Error lexing '${text}' at position ${event.offset}` ); + return [ document.createTextNode( text ) ]; + } + + if ( event.isDone ) + { + continue; + } + + if ( TemplatesSourceLexer.DEFAULT === event.type ) + { + defaultValue = TemplatesSourceLexer.defaultMatcher.getMatch( subMatchValue, 1 ); + continue; + } + + + if ( TemplatesSourceLexer.CONTENT === event.type ) + { + outputs.push( document.createTextNode( subMatchValue ) ); + } + else + { + TemplateSourceMatchers.regex.lastIndex = 0; + let subMatch = TemplateSourceMatchers.regex.exec( subMatchValue ); + let templateSource = TemplateSourceMatchers.createTemplateSource( subMatch ); + + if ( defaultValue ) + { + templateSource.defaultValue = defaultValue; + } + + let targetNodes = templateSource.getNodes( target ); + outputs = outputs.concat( targetNodes ); + //targetNodes.forEach( n => outputs.push( n ) ); + + //let sourceValue = templateSource.getInnerHTML( target ); + //outputs.push( sourceValue ); + } + + defaultValue = null; + + } + + return outputs; + + } + + + + /* + static createElementSource( match:RegExpExecArray ) + { + let elementSource = new TemplateSource(); + + //console.log( "Create Element SOurce", match ); + + if ( match[ 1 ] ) + { + elementSource.selector = match[ 1 ]; + elementSource.attribute = match[ 2 ]; + } + else if ( match[ 3 ] ) + { + elementSource.selector = match[ 3 ]; + } + else if ( match[ 4 ] ) + { + elementSource.attribute = match[ 4 ]; + } + else if ( match[ 5 ] ) + { + elementSource.innerHTML = true; + } + + return elementSource; + } + + */ + +} \ No newline at end of file diff --git a/browser/templates/TemplateSource.ts b/browser/templates/TemplateSource.ts new file mode 100644 index 0000000..6d9d31c --- /dev/null +++ b/browser/templates/TemplateSource.ts @@ -0,0 +1,132 @@ +import { DOMEditor as HTMLEditor } from "../dom/DOMEditor"; + +export type SpecialTemplateSourceElementCallback = + (special:string,selector:string,attribute:string) => Node[]; + +export class SpecialTemplateSourceElement +{ + _specialCallback:SpecialTemplateSourceElementCallback; + + static assignSpecialTemplateSource( element:Element, callback:SpecialTemplateSourceElementCallback ) + { + let sp = element as any as SpecialTemplateSourceElement; + sp._specialCallback = callback; + + // console.log( "Adding speical cb to:", element.nodeName ); + } + + static getNodes( element:Element, templateSource:TemplateSource):Node[] + { + let sp = element as any as SpecialTemplateSourceElement; + + if ( ! sp._specialCallback ) + { + console.log( "NOT ASSIGNED SPECIAL:", element.nodeName, templateSource.special, templateSource.selector, templateSource.attribute ); + return []; + } + + let special = templateSource.special; + let selector = templateSource.selector; + let attribute = templateSource.attribute; + + let result = sp._specialCallback( special, selector, attribute ); + + //console.log( "LOADED SPECIAL:", templateSource.special, templateSource.selector, templateSource.attribute, "\n>>", result ); + + return result; + } + +} + +export class TemplateSource +{ + special:string; + selector:string; + attribute:string; + innerHTML:boolean; + defaultValue:string; + copyParent:boolean; + + getInnerHTML( target:Element ) + { + if ( this.innerHTML ) + { + return target.innerHTML; + } + + let nodes = this.getNodes( target ); + let container = target.ownerDocument.createElement( "div" ) as HTMLElement; + nodes.forEach( n => container.appendChild( n ) ); + + return container.innerHTML; + } + + + get emptyNodes():Node[] + { + if ( this.defaultValue ) + { + return [ document.createTextNode( this.defaultValue ) ]; + } + + return []; + } + + getNodes( target:Element ):Node[] + { + if ( this.innerHTML ) + { + return HTMLEditor.cloneChildren( target ); + } + else if ( this.special ) + { + return SpecialTemplateSourceElement.getNodes( target, this ); + } + else if ( this.attribute ) + { + let attributTarget = this.selector ? target.querySelector( this.selector ) : target; + + if ( ! attributTarget ) + { + return this.emptyNodes; + } + + if ( ! attributTarget.hasAttribute( this.attribute ) ) + { + return this.emptyNodes; + } + + let attributeValue = attributTarget.getAttribute( this.attribute ); + + + let textNode = target.ownerDocument.createTextNode( attributeValue ); + + return [ textNode ]; + } + else + { + + let node = target.querySelector( this.selector ); + + if ( ! node ) + { + return this.emptyNodes; + } + + if ( this.copyParent ) + { + return [ target.ownerDocument.importNode( node, true ) ]; + } + + let nodes = []; + + for ( let i = 0; i < node.childNodes.length; i++ ) + { + let clone = target.ownerDocument.importNode( node.childNodes[ i ], true ); + nodes.push( clone ); + } + + return nodes; + } + } +} \ No newline at end of file diff --git a/browser/templates/TemplateSourceMatchers.ts b/browser/templates/TemplateSourceMatchers.ts new file mode 100644 index 0000000..ac8c1b5 --- /dev/null +++ b/browser/templates/TemplateSourceMatchers.ts @@ -0,0 +1,118 @@ +import { ExtendedRegex } from "../text/ExtendedRegex"; +import { TemplateSource } from "./TemplateSource"; +import { TemplatesSourceLexer } from "./TemplatesSourceLexer"; + +export enum TemplateMatchType +{ + SPECIAL, + SELECTOR, + ATTRIBUTE, + PASS_THROUGH +} + +export class TemplateSourceMatchers +{ + static readonly regex = ExtendedRegex.create( + /@\{(\P):(\V)\}\[(\P)\]|@\{(\V)\}\[(\P)\]|@\{(\P):(\V)\}|@\{(\V)\}|@\[(\P)\]|(@\{\})/g + ); + + + static readonly matchers = + [ + { + type: + TemplatesSourceLexer.SPECIAL_SELECTOR_AND_ATTRIBUTE, + values: + [ TemplateMatchType.SPECIAL, TemplateMatchType.SELECTOR, TemplateMatchType.ATTRIBUTE ] + }, + + { + type: + TemplatesSourceLexer.SELECTOR_AND_ATTRIBUTE, + values: + [ TemplateMatchType.SELECTOR, TemplateMatchType.ATTRIBUTE ] + }, + + { + type: + TemplatesSourceLexer.SPECIAL_SELECTOR, + values: + [ TemplateMatchType.SPECIAL, TemplateMatchType.SELECTOR ] + }, + + { + type: + TemplatesSourceLexer.SELECTOR, + values: + [ TemplateMatchType.SELECTOR ] + }, + + { + type: + TemplatesSourceLexer.ATTRIBUTE, + values: + [ TemplateMatchType.ATTRIBUTE ] + }, + + { + type: + TemplatesSourceLexer.PASS_THROUGH, + values: + [ TemplateMatchType.PASS_THROUGH ] + } + ]; + + static createTemplateSource( match:RegExpExecArray ) + { + let elementSource = new TemplateSource(); + + var offset = 1; + + if ( match == null || offset > match.length ) + { + elementSource.defaultValue = ""; + console.log( "No matching value >> ", match ); + return elementSource; + } + + for ( let i = 0; i < this.matchers.length; i++ ) + { + let matcher = this.matchers[ i ]; + + if ( ! match[ offset ] ) + { + offset += matcher.values.length; + continue; + } + + let matchValuesOffset = offset; + + for ( let j = 0; j < matcher.values.length; j++ ) + { + let value = matcher.values[ j ]; + let valueOffset = matchValuesOffset + j; + + if ( TemplateMatchType.SPECIAL === value ) + { + elementSource.special = match[ valueOffset ]; + } + else if ( TemplateMatchType.SELECTOR === value ) + { + elementSource.selector = match[ valueOffset ]; + } + else if ( TemplateMatchType.ATTRIBUTE === value ) + { + elementSource.attribute = match[ valueOffset ]; + } + else if ( TemplateMatchType.PASS_THROUGH === value ) + { + elementSource.innerHTML = true; + } + } + + return elementSource; + } + + return elementSource; + } +} \ No newline at end of file diff --git a/browser/templates/TemplatesManager.ts b/browser/templates/TemplatesManager.ts new file mode 100644 index 0000000..2398c61 --- /dev/null +++ b/browser/templates/TemplatesManager.ts @@ -0,0 +1,241 @@ +import { IncrementalIDGenerator } from "../random/IncrementalIDGenerator"; +import { ElementAttribute } from "../dom/ElementAttribute"; +import { TemplateReplacer } from "./TemplateReplacer"; +import { ElementProcessor } from "./ElementProcessor"; +import { DOMEditor as HTMLEditor } from "../dom/DOMEditor"; +import { StylesProcessor } from "./styles-processor/StylesProcessor"; +import { ElementType } from "../dom/ElementType"; + + +export enum TemplatesManagerMode +{ + ADD_STYLES_TO_HEAD, + IGNORE_STYLES +} + +export class TemplatesManager +{ + static readonly defaultOverloadAttributeName = "templates-overload-type"; + static readonly defaultIDAttributeName = "templates-id"; + static readonly id = new ElementAttribute( TemplatesManager.defaultIDAttributeName ); + + private _overloadAttribute = new ElementAttribute( TemplatesManager.defaultOverloadAttributeName ); + private _idAttribute = new ElementAttribute( TemplatesManager.defaultIDAttributeName ); + + private _templatesMap:Map = new Map(); + private _idGenerator:IncrementalIDGenerator = new IncrementalIDGenerator(); + private _replacer:TemplateReplacer = new TemplateReplacer(); + get replacer(){ return this._replacer; } + private _mode = TemplatesManagerMode.ADD_STYLES_TO_HEAD; + private _templateStyles:string[] = []; + private _additionalProcessor = new Map(); + + setMode( mode:TemplatesManagerMode ) + { + this._mode = mode; + } + + addProcessor( processor:ElementProcessor ) + { + this._additionalProcessor.set( processor.getElementName(), processor ); + } + + addTemplate( element:Element ) + { + + if ( Node.ELEMENT_NODE !== element.nodeType ) + { + return; + } + + if ( "STYLE" === element.nodeName.toUpperCase() ) + { + this._addStyle( element ); + return; + } + + let hash = element.nodeName; + + if ( this._overloadAttribute.in( element ) ) + { + let overloadType = this._overloadAttribute.from( element ); + hash += " " + overloadType; + } + + this._templatesMap.set( hash, element ); + } + + static templateIDCounter = 0; + addTemplateHTML( html:string ) + { + TemplatesManager.templateIDCounter ++; + + let generatedDocument = HTMLEditor.stringToDocument( `${html}` ); + + let children = HTMLEditor.nodeListToArray( generatedDocument.body.childNodes ); + + for ( let i = 0; i < children.length; i++ ) + { + this.addTemplate( children[ i ] ); + } + } + + + processChildren( root:Element ) + { + let children = HTMLEditor.nodeListToArray( root.childNodes ); + + for ( let i = 0; i < children.length; i++ ) + { + this._processElement( children[ i ] ); + } + } + + processElement( element:Element ) + { + return this._processElement( element ); + } + + getTemplate( nodeName:string ) + { + return this._templatesMap.get( nodeName.toUpperCase() ); + } + + createTemplate( nodeName:string, doc:Document = undefined ) + { + doc = doc || document; + + let templateNode = doc.importNode( this.getTemplate( nodeName ), true ); + templateNode = this._processElement( templateNode ); + + return templateNode; + } + + private _addStyle( styleElement:Element ) + { + if ( TemplatesManagerMode.IGNORE_STYLES === this._mode ) + { + //this._templateStyles.push( styleElement.innerHTML ); + //console.log( "Adding style to templates.css", styleElement ); + return; + } + + let styleContent = styleElement.textContent; + let processedStyleContent = StylesProcessor.convert( styleContent ); + let clonedStyle = ElementType.style.create(); + clonedStyle.innerHTML = processedStyleContent; + //let clonedStyle = document.importNode( styleElement, true ); + document.head.appendChild( clonedStyle ); + + let id = this._idGenerator.createID(); + this._idAttribute.to( styleElement, id ); + this._idAttribute.to( clonedStyle, id ); + + //console.log( "Adding style to head", styleElement, clonedStyle ); + } + + private _processElement( element:Element ):Element + { + //console.log( "Processing:", HierarchyName.of( element ) ); + + if ( this._idAttribute.in( element ) ) + { + this.processChildren( element ); + return element; + } + + if ( this._additionalProcessor.has( element.nodeName ) ) + { + let processor = this._additionalProcessor.get( element.nodeName ); + let processedElement:Element = null; + + //console.log( "ADDITIONAL PROCESSOR", processor ); + try + { + processedElement = processor.processElement( element ); + } + catch ( e ) + { + console.log( "Processor:", processor, `ERROR: in <${element.nodeName.toLowerCase()}>` ); + throw e; + } + + this._idAttribute.to( processedElement, this._idGenerator.createID() ); + //console.log( "Replacing Kids:", HierarchyName.of( processedElement ) ); + + this._replacer.replaceChildren( element, processedElement ); + + //console.log( "PROCESSING Kids:", HierarchyName.of( processedElement ) ); + this.processChildren( processedElement ); + + if ( processor.hasPostProcessor ) + { + processor.postProcessElement( element, processedElement ); + } + + + return processedElement; + } + + if ( ! this._templatesMap.has( element.nodeName ) ) + { + this.processChildren( element ); + return element; + } + + let classElement = this._templatesMap.get( element.nodeName.toUpperCase() ); + + let replacedElement:Element = null; + + try + { + replacedElement = this._replacer.replace( element, classElement ); + } + catch ( e ) + { + console.log( `Replacer-Error: in <${element.nodeName.toLowerCase()}>`, e.stack ); + throw e; + } + + + + //console.log( "CLASS ELEMENT", classElement, replacedElement ); + this._idAttribute.to( replacedElement, this._idGenerator.createID() ); + + this.processChildren( replacedElement ); + + return replacedElement; + + } + + static addCustomElements() + { + let customElementsManager = new TemplatesManager(); + + let customElements = document.querySelector( "custom-elements" ); + + if ( ! customElements ) + { + return; + } + + let customElementsNodes = customElements.childNodes; + + + for ( let i = 0; i < customElementsNodes.length; i++ ) + { + if ( Node.ELEMENT_NODE != customElementsNodes[ i ].nodeType ) + { + continue; + } + + //console.log( customElementsNodes[ i ].nodeName ); + + customElementsManager.addTemplate( customElementsNodes[ i ] as Element); + } + + HTMLEditor.remove( customElements ); + + customElementsManager.processChildren( document.body ); + } +} \ No newline at end of file diff --git a/browser/templates/TemplatesSourceLexer.ts b/browser/templates/TemplatesSourceLexer.ts new file mode 100644 index 0000000..6b94d6c --- /dev/null +++ b/browser/templates/TemplatesSourceLexer.ts @@ -0,0 +1,78 @@ +import { Lexer } from "../text/lexer/Lexer"; +import { LexerMatcher } from "../text/lexer/LexerMatcher"; +import { ExtendedRegex } from "../text/ExtendedRegex"; + +export type SPECIAL_SELECTOR_AND_ATTRIBUTE = "SPECIAL_SELECTOR_AND_ATTRIBUTE"; +export type SELECTOR_AND_ATTRIBUTE = "SELECTOR_AND_ATTRIBUTE"; +export type SPECIAL_SELECTOR = "SPECIAL_SELECTOR"; +export type SELECTOR = "SELECTOR"; +export type ATTRIBUTE = "ATTRIBUTE"; +export type PASS_THROUGH = "PASS_THROUGH"; +export type CONTENT = "CONTENT"; +export type DEFAULT = "DEFAULT"; + +export type TemplatesSourceType = SELECTOR_AND_ATTRIBUTE | SELECTOR | ATTRIBUTE | PASS_THROUGH; + +export class TemplatesSourceLexer extends Lexer +{ + static readonly SPECIAL_SELECTOR_AND_ATTRIBUTE:SPECIAL_SELECTOR_AND_ATTRIBUTE = "SPECIAL_SELECTOR_AND_ATTRIBUTE"; + static readonly SELECTOR_AND_ATTRIBUTE:SELECTOR_AND_ATTRIBUTE = "SELECTOR_AND_ATTRIBUTE"; + static readonly SPECIAL_SELECTOR:SPECIAL_SELECTOR = "SPECIAL_SELECTOR"; + static readonly SELECTOR:SELECTOR = "SELECTOR"; + + static readonly ATTRIBUTE:ATTRIBUTE = "ATTRIBUTE"; + static readonly PASS_THROUGH:PASS_THROUGH = "PASS_THROUGH"; + static readonly CONTENT:CONTENT = "CONTENT"; + + static readonly DEFAULT:DEFAULT = "DEFAULT"; + + static readonly defaultMatcher = new LexerMatcher( + TemplatesSourceLexer.DEFAULT, ExtendedRegex.create( /@\(\(([^\)\)]+)\)\)/ ) + ); + + static readonly specialSelectorAndAttributeMatcher = new LexerMatcher( + TemplatesSourceLexer.SPECIAL_SELECTOR_AND_ATTRIBUTE, ExtendedRegex.create( /@\{(\P):(\V)\}\[(\P)\]/ ) + ); + + static readonly selectorAndAttributeMatcher = new LexerMatcher( + TemplatesSourceLexer.SELECTOR_AND_ATTRIBUTE, ExtendedRegex.create( /@\{(\V)\}\[(\P)\]/ ) + ); + + static readonly specialSelectorMatcher = new LexerMatcher( + TemplatesSourceLexer.SELECTOR, ExtendedRegex.create( /@\{(\P):(\V)\}/ ) + ); + + static readonly selectorMatcher = new LexerMatcher( + TemplatesSourceLexer.SELECTOR, ExtendedRegex.create( /@\{(\V)\}/ ) + ); + + static readonly attributeMatcher = new LexerMatcher( + TemplatesSourceLexer.ATTRIBUTE, ExtendedRegex.create( /@\[(\P)\]/ ) + ); + + static readonly passThroughMatcher = new LexerMatcher( + TemplatesSourceLexer.ATTRIBUTE, ExtendedRegex.create( /@\{\}/ ) + ); + + static readonly contentMatcher = new LexerMatcher( + TemplatesSourceLexer.CONTENT, ExtendedRegex.create( /.|\n|\r/ ) + ) + + constructor() + { + super(); + this._create(); + } + + _create() + { + this.addMatcher( TemplatesSourceLexer.defaultMatcher ); + this.addMatcher( TemplatesSourceLexer.specialSelectorAndAttributeMatcher ); + this.addMatcher( TemplatesSourceLexer.selectorAndAttributeMatcher ); + this.addMatcher( TemplatesSourceLexer.specialSelectorMatcher ); + this.addMatcher( TemplatesSourceLexer.selectorMatcher ); + this.addMatcher( TemplatesSourceLexer.attributeMatcher ); + this.addMatcher( TemplatesSourceLexer.passThroughMatcher ); + this.addMatcher( TemplatesSourceLexer.contentMatcher ) ; + } +} \ No newline at end of file diff --git a/browser/templates/styles-processor/StylesProcessor.ts b/browser/templates/styles-processor/StylesProcessor.ts new file mode 100644 index 0000000..b866ed9 --- /dev/null +++ b/browser/templates/styles-processor/StylesProcessor.ts @@ -0,0 +1,11 @@ +import { StylesProcessorLexer } from "./StylesProcessorLexer"; + +export class StylesProcessor +{ + static convert( styles:string ) + { + let lexer = new StylesProcessorLexer(); + return lexer.convert( styles ); + } + +} \ No newline at end of file diff --git a/browser/templates/styles-processor/StylesProcessorLexer.ts b/browser/templates/styles-processor/StylesProcessorLexer.ts new file mode 100644 index 0000000..4c093c6 --- /dev/null +++ b/browser/templates/styles-processor/StylesProcessorLexer.ts @@ -0,0 +1,168 @@ +import { Lexer } from "../../text/lexer/Lexer"; +import { LexerEvent } from "../../text/lexer/LexerEvent"; +import { LexerMatcher } from "../../text/lexer/LexerMatcher"; +import { LexerMatcherLibrary } from "../../text/lexer/LexerMatcherLibrary"; +import { LexerType, LexerTypes } from "../../text/lexer/LexerType"; +import { LexerQuery } from "../../text/lexer/LexerQuery"; +import { LiteralLexerEventTypeMatcher } from "../../text/lexer/LexerEventMatcher"; +import { StylesProcessor } from "./StylesProcessor"; + +export type PORTRAIT_MARKER = "PORTRAIT_MARKER"; +export type LANDSCAPE_MARKER = "LANDSCAPE_MARKER"; +export type ROOT_ELEMENT_REPLACER = "ROOT_ELEMENT_REPLACER"; + +export type StylesProcessorLexerType = LexerType | ROOT_ELEMENT_REPLACER | PORTRAIT_MARKER | LANDSCAPE_MARKER; + +export class StylesProcessorLexer extends Lexer +{ + static readonly PORTRAIT_MARKER:PORTRAIT_MARKER = "PORTRAIT_MARKER"; + static readonly PORTRAIT_MARKER_MATCHER = + new LexerMatcher( StylesProcessorLexer.PORTRAIT_MARKER, /#portrait/i ); + + static readonly LANDSCAPE_MARKER:LANDSCAPE_MARKER = "LANDSCAPE_MARKER"; + static readonly LANDSCAPE_MARKER_MATCHER = + new LexerMatcher( StylesProcessorLexer.LANDSCAPE_MARKER, /#landscape/i ); + + static readonly ROOT_ELEMENT_REPLACER:ROOT_ELEMENT_REPLACER = "ROOT_ELEMENT_REPLACER"; + static readonly ROOT_ELEMENT_REPLACER_MATCHER = + new LexerMatcher( StylesProcessorLexer.ROOT_ELEMENT_REPLACER, /\[_?\]/ ); + constructor() + { + super(); + + this.addAllMatchers( + + LexerMatcherLibrary.SINGLE_LINE_COMMENT_MATCHER, + LexerMatcherLibrary.MULTI_LINE_COMMENT_MATCHER, + LexerMatcherLibrary.DOUBLE_QUOTED_STRING_MATCHER, + LexerMatcherLibrary.SINGLE_QUOTED_STRING_MATCHER, + LexerMatcherLibrary.WHITESPACE_MATCHER, + LexerMatcherLibrary.BLOCKSTART_MATCHER, + LexerMatcherLibrary.BLOCKEND_MATCHER, + StylesProcessorLexer.PORTRAIT_MARKER_MATCHER, + StylesProcessorLexer.LANDSCAPE_MARKER_MATCHER, + StylesProcessorLexer.ROOT_ELEMENT_REPLACER_MATCHER, + LexerMatcherLibrary.CSS_CLASS_SELECTOR_MATCHER, + LexerMatcherLibrary.CSS_ID_SELECTOR_MATCHER, + LexerMatcherLibrary.CSS_WORD_MATCHER, + LexerMatcherLibrary.ANY_SYMBOL_MATCHER + ); + + } + + getCompressedTokens( source:string ) + { + let events = this.lexToList( source ); + let types = new Set(); + types.add( LexerMatcherLibrary.WHITESPACE_MATCHER.type ); + types.add( LexerMatcherLibrary.ANY_SYMBOL_MATCHER.type ); + + events = this.compress( events, types ); + + return events; + } + + convert( source:string ) + { + let tokens = this.getCompressedTokens( source ); + this.resolveMatches( source, tokens ); + let query = new LexerQuery(); + query.source = source; + query.tokens = tokens; + + let rootElement = ""; + + query.forAllWithType( LexerTypes.CSS_WORD, + ( t ) => + { + if ( rootElement !== "" ) + { + return; + } + + let isCustomElement = LexerMatcherLibrary.HTML_CUSTOM_ELEMENT_MATCHER.isMatching( t.match ); + + console.log( "IsCustom:", t.match, isCustomElement ); + if ( ! isCustomElement ) + { + return; + } + + rootElement = t.match; + } + ); + + let emptyRootElementMessage = false; + + query.forAllWithType( StylesProcessorLexer.ROOT_ELEMENT_REPLACER, + ( t )=> + { + if ( rootElement == "" && ! emptyRootElementMessage ) + { + emptyRootElementMessage = true; + console.log( "No root element detected for [] replacements!" ); + } + + t.setMatch( rootElement ); + } + ); + + let output:string[] = []; + + let markerReplacements = + { + [ StylesProcessorLexer.PORTRAIT_MARKER ]: "@media ( orientation:portrait )", + [ StylesProcessorLexer.LANDSCAPE_MARKER ]: "@media ( orientation:landscape )", + } + + + + let start = new LiteralLexerEventTypeMatcher( LexerTypes.BLOCKSTART ); + let end = new LiteralLexerEventTypeMatcher( LexerTypes.BLOCKEND ); + + for ( let i = 0; i < tokens.length; i++ ) + { + let token = tokens[ i ]; + + if ( + token.isType( StylesProcessorLexer.PORTRAIT_MARKER ) || + token.isType( StylesProcessorLexer.LANDSCAPE_MARKER ) + ) + { + let blockIndices = query.searchBlockIndices( i, start, end ); + + if ( blockIndices == null ) + { + output.push( token.match ); + } + else + { + let markerType = token.type as ( PORTRAIT_MARKER | LANDSCAPE_MARKER ); + let replacement = markerReplacements[ markerType ]; + output.push( replacement ); + output.push( "{" ); + + + query.forAllMatches( + i + 1 , blockIndices.endIndex, + ( match ) => + { + output.push( match ); + } + ); + + output.push( "}" ); + + i = blockIndices.endIndex; + } + } + else + { + output.push( token.match ); + } + } + + + return output.join( "" ); + } +} \ No newline at end of file diff --git a/browser/text/ExtendedRegex.ts b/browser/text/ExtendedRegex.ts new file mode 100644 index 0000000..c484fc6 --- /dev/null +++ b/browser/text/ExtendedRegex.ts @@ -0,0 +1,96 @@ +import { RegExpUtility } from "./RegExpUtitlity"; + + +export class ExtendedRegex +{ + private static _extensions:Map = null; + + static create( regex:string|RegExp, flags:string = undefined ) + { + if ( typeof regex === "string" ) + { + let rgx = new RegExp( ExtendedRegex.parseSource( regex ), flags ); + return rgx; + } + else + { + let source = regex.source; + let parsed = ExtendedRegex.parseSource( source ); + flags = flags || regex.flags; + let rgx = new RegExp( parsed, flags ); + return rgx; + } + } + + static get extensions() + { + if ( ExtendedRegex._extensions ) + { + return ExtendedRegex._extensions; + } + + let extensions:any = {}; + + // \P = Property, word with hyphen + extensions[ "\\\\P" ] = "(?:\\w|\\-)+"; + // \V = CSS Attribute Value + extensions[ "\\\\V" ] = "(?:\\w|\-|\\||^|$|~|=|#|\\*|\\.|\\\"|\\\'|\\[|\\]|\\s)+"; + + // \a = Vowels en + extensions[ "\\\\a" ] = "[aeiouAEIOU]"; + // \A = Not Vowels en + extensions[ "\\\\A" ] = "[^aeiouAEIOU]"; + + // \y = Vowels en with y + extensions[ "\\\\y" ] = "[aeiouyAEIOUY]"; + // \Y = Not Vowels en with y + extensions[ "\\\\Y" ] = "[^aeiouyAEIOUY]"; + + // \ä = Vowels de extended + extensions[ "\\\\ä" ] = "[aeiouyAEIOUYäöüÄÖÜ]"; + // \Ä = Not Vowels de extended + extensions[ "\\\\Ä" ] = "[^aeiouyAEIOUYäöüÄÖÜ]"; + + // \k = Consonants + extensions[ "\\\\k" ] = "[b-df-hj-np-tv-zB-DF-HJ-NP-TV-Z]"; + + // \K = Not Consonants + extensions[ "\\\\K" ] = "[^b-df-hj-np-tv-zB-DF-HJ-NP-TV-Z]"; + + // \z = Any Break + extensions[ "\\\\z" ] = "(?:.|\n|\r)*?"; + + // \h = Hex Only lower case + extensions[ "\\\\h" ] = "[a-z0-9]"; + + // \H = Hex + extensions[ "\\\\H" ] = "[a-zA-Z0-9]"; + + + ExtendedRegex._extensions = new Map(); + + for ( let key in extensions ) + { + let regexSource = RegExpUtility.toRegexSource( key ); + let regex = new RegExp( regexSource, "g" ); + + ExtendedRegex._extensions.set( regex, extensions[ key ] ); + } + + return ExtendedRegex._extensions; + } + + + private static parseSource( source:string ) + { + for ( let entry of ExtendedRegex.extensions ) + { + let regex = entry[ 0 ]; + let replacement = entry[ 1 ]; + + source = source.replace( regex, replacement ); + } + + return source; + } +} \ No newline at end of file diff --git a/browser/text/Levehshtein.ts b/browser/text/Levehshtein.ts new file mode 100644 index 0000000..aae4832 --- /dev/null +++ b/browser/text/Levehshtein.ts @@ -0,0 +1,108 @@ + +export class LevenshteinFuzzyMatch +{ + lowestDistance:number; + index:number; +} + +export class LevenshteinDistance +{ + protected static getMatrix( aLength:number, bLength:number ) + { + let matrix:number[][] = []; + + for ( let i = 0; i < aLength + 1; i++ ) + { + let bArray = []; + + for ( let j = 0; j < bLength + 1; j++ ) + { + bArray.push( 0 ); + } + + matrix.push( bArray ); + } + + for ( let i = 0; i <= aLength; i++ ) + { + matrix[ i ][ 0 ] = i; + } + + for ( let j = 0; j <= bLength; j++ ) + { + matrix[ 0 ][ j ] = j; + } + + return matrix; + } + + static computeFuzzyMatch( value:string, matcher:string ) + { + let result = new LevenshteinFuzzyMatch(); + + if ( matcher.length >= value.length ) + { + result.index = 0; + result.lowestDistance = LevenshteinDistance.compute( value, matcher ); + + } + else + { + let offsets = value.length - matcher.length; + + for ( let i = 0; i < offsets; i++ ) + { + let subValue = value.substring( i, i + matcher.length ); + let distance = LevenshteinDistance.compute( subValue, matcher ); + + if ( i === 0 ) + { + result.index = 0; + result.lowestDistance = distance; + continue; + } + + if ( distance < result.lowestDistance ) + { + result.index = i; + result.lowestDistance = distance; + } + + } + } + + + return result; + } + + + static compute( a:string, b:string ):number + { + let aLength = a.length; + let bLength = b.length; + + if ( aLength == 0 ) { return bLength; } + if ( bLength == 0 ) { return aLength; } + + let evaluationMatrix = LevenshteinDistance.getMatrix( aLength, bLength ); + + for ( let i = 1; i <= aLength; i++ ) + { + for ( let j = 1; j <= bLength; j++ ) + { + let isSame = b[ j - 1 ] === a[ i - 1 ]; + + let isDifferentCost = isSame ? 0 : 1; + + let insertionCost = evaluationMatrix[ i - 1][ j ] + 1; + let deletionCost = evaluationMatrix[ i ][ j - 1 ] + 1; + let substitutionCost = evaluationMatrix[ i - 1][ j - 1 ] + isDifferentCost; + + evaluationMatrix[ i ][ j ] = Math.min( insertionCost, deletionCost, substitutionCost ); + } + } + + return evaluationMatrix[ aLength][ bLength ]; + } +} + diff --git a/browser/text/RegExpUtitlity.ts b/browser/text/RegExpUtitlity.ts new file mode 100644 index 0000000..18fbeb3 --- /dev/null +++ b/browser/text/RegExpUtitlity.ts @@ -0,0 +1,952 @@ +import { MathX } from "../math/MathX"; +import { Arrays } from "../tools/Arrays"; +import { LevenshteinDistance } from "./Levehshtein"; +import { LexerMatcher } from "./lexer/LexerMatcher"; + +export class RegExpUtility +{ + static repeat( text:string, times:number ) + { + let output = []; + + while ( times > 0 ) + { + output.push( text ); + times--; + } + + return output.join( "" ); + } + + static collapse( text:string, collapsingPattern:string ) + { + let collapsingRegex = /((?:XXX)+)/g; + let escapedRegexContent = collapsingRegex.source; + let escapedCollapsedPattern = RegExpUtility.toRegexSource( collapsingPattern ); + escapedRegexContent = escapedRegexContent.replace( "XXX", escapedCollapsedPattern ); + + let regex = new RegExp( escapedRegexContent, "g" ); + + return text.replace( regex, collapsingPattern ); + } + + static collapseWhiteSpace( text:string ) + { + text = text.replace( /((?:\s|\n|\r|\t)+)/g, " " ); + + return text.trim(); + } + + static toFileName( text:string ) + { + let fileNameOutput:string[] = []; + + for ( let i = 0; i < text.length; i++ ) + { + let character = text[ i ]; + + if ( /[a-zA-Z0-9\-]/.test( character ) ) + { + fileNameOutput.push( character ); + //console.log( "Not escaped:", character ); + } + else + { + fileNameOutput.push( "_" ); + //console.log( "Escaped:", character, ">>", "_" ); + } + } + + return fileNameOutput.join( "" ); + + } + + static trimCharacters( text:string, fromStart:number, fromEnd:number ) + { + let start = fromStart; + let end = text.length - fromEnd; + + return text.substring( start, end ); + } + + static splitLines( text:string ) + { + return text.split( /(?:\r\n)|\n|\r/g ); + } + + static splitLinesCaptureBreaks( text:string ) + { + return text.split( /((?:\r\n)|\n|\r)/g ); + } + + static createMatcherFromCombiningWords( words:string[], noSubmatches:boolean ):RegExp + { + let sources = words.map( w => RegExpUtility.toRegexSource( w ) ); + + let regexSource = sources.join( "|" ); + + if ( ! noSubmatches ) + { + regexSource = `^(${regexSource})$`; + } + + + return new RegExp( regexSource ); + } + + static matches( value:string, matcher:string|RegExp ) + { + if ( ! matcher ) + { + return true; + } + + if ( typeof matcher === "string" ) + { + return value === matcher; + } + + return matcher.test( value ); + } + + static cutout( value:string, regex:RegExp ):string[] + { + let result = regex.exec( value ); + + if ( ! result ) + { + return [ value, null ]; + } + + let match = result[ 0 ]; + + let cutout = RegExpUtility.cutoutRange( value, result.index, match.length ); + + return [ cutout, match ]; + } + + static chopRange( value:string, start:number, end:number ) + { + let chopped:string[] = []; + + if ( start > 0 ) + { + chopped.push( value.substring( 0, start ) ); + } + else + { + chopped.push( null ); + } + + chopped.push( value.substring( start, end ) ); + + if ( end < value.length ) + { + chopped.push( value.substring( end, value.length ) ); + } + else + { + chopped.push( null ); + } + + return chopped; + } + + static cutoutRange( value:string, start:number, length:number ):string + { + if ( start == 0 ) + { + return value.substring( length ); + } + + let before = value.substring( 0, start ); + + let after = value.substring( start + length ); + + return before + after; + } + + static parseDuration( value:string, alternative:number = 0 ) + { + if ( ! value ) + { + return alternative; + } + + if ( value.indexOf( ":" ) === -1 ) + { + return parseFloat( value ); + } + + let values = this.parseNumbers( value, [ 0, 0 ], ":" ); + + return values[ 0 ] * 60 + values[ 1 ]; + } + + static asDurationString( duration:number, delimiter:string = ":", zerofillMinutes:boolean = false ) + { + let seconds = Math.floor( MathX.repeat( duration, 60 ) ) + ""; + let minutes = Math.floor( duration / 60 ) + ""; + + if ( seconds.length < 2 ){ seconds = "0" + seconds; } + if ( zerofillMinutes && minutes.length < 2 ){ minutes = "0" + minutes; } + + return minutes + delimiter + seconds; + } + + static parseNumbers( value:string, alternative:number[] = [], delimiter = "," ) + { + if ( ! value ) + { + return alternative; + } + + var splitted = value.split( delimiter ); + + var numberValues = []; + + for ( let i = 0; i < splitted.length; i++ ) + { + var splitValue = splitted[ i ].trim(); + var numericValue = parseFloat( splitValue ); + + numberValues.push( numericValue ); + } + + return numberValues; + } + + + static splitPath( path:string ) + { + return path.split( /\\|\// ); + } + + static startsWithSome( text:string, starts:string[] ) + { + return starts.some( s => text.startsWith( s ) ); + } + + static escapePathFragmentForWindows( fragmentWithoutSlashes:string, replacement:string = "_" ):string + { + return fragmentWithoutSlashes.replace( /(\\|\/|\?|\:|\||\*|\<|\>)/g, replacement ); + } + + static resolvePath( path:string ) + { + let pathFragments = path.split( "/" ); + let resolvedFragments = []; + + for ( let i = 0; i < pathFragments.length; i++ ) + { + if ( pathFragments[ i ] === ".." ) + { + if ( resolvedFragments.length === 0 || resolvedFragments[ i - 1 ] === ".." ) + { + resolvedFragments.push( ".." ); + } + else + { + resolvedFragments.pop(); + } + + } + else + { + resolvedFragments.push( pathFragments[ i ] ); + } + } + + let resolvedPath = resolvedFragments.join( "/" ); + return resolvedPath; + } + + static createRelativeDirectoryPath( sourceDirectory:string, targetDirectory:string) + { + if ( sourceDirectory === targetDirectory ) + { + return ""; + } + + sourceDirectory = this.normalizePath( sourceDirectory ); + targetDirectory = this.normalizePath( targetDirectory ); + + if ( sourceDirectory === targetDirectory ) + { + return ""; + } + + + let matching = RegExpUtility.getMatchingDirectories( sourceDirectory, targetDirectory ); + + + let shortSource = sourceDirectory.substring( matching.length ); + let shortTarget = targetDirectory.substring( matching.length ); + + shortSource = this.normalizePath( shortSource ); + shortTarget = this.normalizePath( shortTarget ); + + if ( this.isEmptyPath( shortSource ) ) + { + return shortTarget; + } + + let sourceDirectories = RegExpUtility.splitPath( shortSource ); + + let path = ""; + + for ( let i = 0; i < sourceDirectories.length; i++ ) + { + if ( path !== "" ) + { + path += "/"; + } + + path += ".."; + + } + + if ( path !== "" ) + { + path += "/" + } + + path += shortTarget; + return path; + } + + static isEmptyPath( path:string ) + { + return /^\s*(\\|\/)?\s*$/.test( path ); + } + + static getAllMatchResultsOf( source:string, regex:RegExp ) + { + let matcher = new LexerMatcher( "match", RegExpUtility.makeSticky( regex ) ); + let offset = 0; + + let results:RegExpExecArray[] = []; + + while ( offset < source.length ) + { + let result = matcher.getMatchResult( source, offset ); + + if ( result ) + { + results.push( result ); + offset += result[ 0 ].length; + } + else + { + offset ++; + } + } + + return results; + + + } + + static removeLeadingSlashes( text:string ) + { + return text.replace( /^\s*(\\|\/)/, "" ); + } + + static removeTrailingSlashes( text:string ) + { + return text.replace( /(\\|\/)\s*$/, "" ); + } + + static trimSlashes( text:string ) + { + return this.removeTrailingSlashes( this.removeLeadingSlashes( text ) ); + } + + static removeLeadingSpace( textContent:string, removeFirstEmpty:boolean = true ) + { + var lines = textContent.split( /(?:\r\n|\r|\n)/ ); + + if ( removeFirstEmpty && /^\s*$/.test( lines[ 0 ] )) + { + lines.shift(); + } + + var offset:number|null = null; + + lines.forEach( + ( line ) => + { + if ( /^\s*$/.test( line ) ) + { + return; + } + + var result = /^\s+/.exec( line ); + + //console.log( "LINE RESULT:", result, "line:", line ); + + if ( result == null ) + { + return; + } + + var numSpaces = result[ 0 ].length; + + + offset = offset === null ? numSpaces : Math.min( offset, numSpaces ); + } + ) + + //console.log( "offset", offset ); + + if ( offset == null ) + { + return lines.join( "\n" ); + } + + lines = lines.map( line => + { + if ( /^\s*$/.test( line ) ) + { + return line; + } + + var result = /^\s+/.exec( line ); + if ( result === null ) + { + return line; + } + + return line.substring( offset ); + } + ); + + return lines.join( "\n" ); + } + + static getMatchingStartOfAll( s:string[] ):string + { + if ( s === null || s === undefined || s.length === 0 ) + { + return ""; + } + + let empty = s.some( e => e === null || e === undefined || e === "" ); + + if ( empty ) + { + return ""; + } + + if ( s.length === 1 ) + { + return s[ 0 ]; + } + + let length = s.map( e => e.length ).reduce( ( a, b ) => Math.min( a,b ) ); + + for ( let i = 0; i < length; i++ ) + { + let value = s[ 0 ][ i ]; + + for ( let j = 1; j < s.length; j ++ ) + { + if ( s[ j ][ i ] !== value ) + { + return s[ 0 ].substring( 0, i ); + } + } + } + + return s[ 0 ].substring( 0, length ); + + + } + + static getMatchingDirectories( a:string, b:string ) + { + let matching = []; + + let directoriesA = this.splitPath( a ); + let directoriesB = this.splitPath( b ); + + for ( let i = 0; i < directoriesA.length && i < directoriesB.length; i++ ) + { + if ( directoriesA[ i ] == directoriesB[ i ] ) + { + matching.push( directoriesA[ i ] ); + } + else + { + return matching.join( "/" ); + } + } + + return matching.join( "/" ); + } + + static getMatchingStart( a:string, b:string ) + { + if ( ! a || ! b ) + { + return ""; + } + + let length = Math.min( a.length, b.length ); + + for ( let i = 0; i < length; i++ ) + { + if ( a[ i ] !== b[ i ] ) + { + return a.substring( 0, i ); + } + } + + if ( length === a.length ) + { + return a; + } + + return b; + } + + static getMatchingEnd( a:string, b:string ) + { + if ( ! a || ! b ) + { + return ""; + } + + let length = Math.min( a.length, b.length ); + + for ( let i = 0 ; i < length; i++ ) + { + let aIndex = a.length - ( 1 + i ); + let bIndex = b.length - ( 1 + i ); + + if ( a[ aIndex ] !== b[ bIndex ] ) + { + return a.substring( aIndex + 1, a.length ); + } + } + + if ( length === a.length ) + { + return a; + } + + return b; + } + + static readonly tagMatchingRegex = + /()((?:.|\n)*)(<\/tag>)/; + + static getTagRegex( tag:string ) + { + return new RegExp( RegExpUtility.tagMatchingRegex.source.replace( /tag/g, tag ) ); + } + + static getTagContent( source:string, tag:string ) + { + let regex = RegExpUtility.getTagRegex( tag ); + + let result = regex.exec( source ); + + if ( ! result ) + { + return null; + } + + return result[ 2 ]; + } + + static setTagContent( source:string, tag:string, value:string ) + { + let regex = RegExpUtility.getTagRegex( tag ); + + return source.replace( regex, `$1${value}$3`); + } + + static ensureSlash( path:string ) + { + if ( path.endsWith( "/" ) ) + { + return path; + } + + return path + "/"; + } + + static minimumDigitsInt( value:number, digits:number = 2 ) + { + let stringValue = Math.round( value ) + ""; + + while ( stringValue.length < digits ) + { + stringValue = "0" + stringValue; + } + + return stringValue; + } + + static round( value:number, numZeros:number ) + { + if ( numZeros <= 0 ) + { + return Math.round( value ) + ""; + } + + numZeros = Math.round( numZeros ); + + var sign = value < 0 ? "-" : ""; + value = Math.abs( value ); + + var roundedBiggerValue = Math.round( value * Math.pow( 10, numZeros ) ); + var stringValue = roundedBiggerValue + ""; + var minimumLength = numZeros + 1; + + while ( stringValue.length < minimumLength ) + { + stringValue = "0" + stringValue; + } + + + var split = stringValue.length - numZeros; + + return sign + stringValue.substring( 0, split ) + "." + stringValue.substring( split ); + } + + static createFromList( list:string[] ) + { + list = list.map( l => RegExpUtility.toRegexSource( l ) ); + + return new RegExp( list.join( "|" ) ); + } + + static toRegexSource( source:string ) + { + source = source.replace( /\./g, "\\." ); + source = source.replace( /\(/g, "\\(" ); + source = source.replace( /\)/g, "\\)" ); + source = source.replace( /\[/g, "\\[" ); + source = source.replace( /\]/g, "\\]" ); + source = source.replace( /\^/g, "\\^" ); + source = source.replace( /\$/g, "\\$" ); + source = source.replace( /\*/g, "\\*" ); + source = source.replace( /\+/g, "\\+" ); + source = source.replace( /\-/g, "\\-" ); + source = source.replace( /\?/g, "\\?" ); + source = source.replace( /\//g, "\\/" ); + source = source.replace( /\|/g, "\\|" ); + + return source; + } + + static upperCaseFirst( source:string ) + { + if ( source === null || source === undefined || source.length === 0 ) + { + return null; + } + + return source[ 0 ].toUpperCase() + source.substring( 1 ); + } + + static lowerCaseFirst( source:string ) + { + if ( source === null || source === undefined || source.length === 0 ) + { + return null; + } + + return source[ 0 ].toLowerCase() + source.substring( 1 ); + } + + private static fileTypeExtensionRegex = /\.(\w+)$/; + + static trimProtocols( link:string, protocols:string[]=["https","http","ftp","sftp"] ) + { + let genericRegex = /^XXX?\:\/\//; + let escapedProtocols = protocols.map( p => RegExpUtility.toRegexSource( p ) ); + let combinedProtocols = `(${escapedProtocols.join("|")})`; + let protocolRegexSource = genericRegex.source.replace( "XXX", combinedProtocols ); + + let regex = new RegExp( protocolRegexSource ); + + // console.log( "REGEX:", regex, ">>", link ); + return link.replace( regex, "" ); + + } + + static trimFileTypeExtension( source:string ) + { + return source.replace( RegExpUtility.fileTypeExtensionRegex, "" ); + } + + static replaceAll( text:string, replacements:Map ) + { + for ( let [ variable, replacement ] of replacements ) + { + let replacementRegexSource = RegExpUtility.toRegexSource( variable ); + let regex = new RegExp( replacementRegexSource, "g" ); + text = text.replace( regex, replacement ); + } + + return text; + } + + static getFileTypeExtension( source:string ) + { + let result = source.lastIndexOf( "." ); + + if ( result === - 1 ) + { + return ""; + } + + return source.substring( result + 1 ); + + } + + static removeStarting( text:string, start:string ) + { + if ( text.startsWith( start ) ) + { + return text.substring( start.length ); + } + + return text; + } + + static removeInner( text:string, start:number, length:number ) + { + return text.substring( 0, start ) + text.substring( start + length ); + } + + static makeSticky( regexp:RegExp ) + { + if ( regexp.sticky ) + { + return regexp; + } + + var source = regexp.source; + var flags = regexp.flags; + + if ( flags.indexOf( "y" ) === -1 ) + { + flags += "y"; + } + + return new RegExp( source, flags ); + } + + static getClosestMatching( list:T[], matching:string, getText:(t:T)=>string = null ):T + { + let index = this.getClosestIndex( list, matching, getText ); + + return list[ index ]; + } + + static getClosestIndex( list:T[], matching:string, getText:(t:T)=>string = null ):number + { + if ( ! getText ) + { + getText = ( t )=>{ return t + "" }; + } + + let index = list.findIndex( l => getText( l ) === matching ); + + if ( index !== -1 ) + { + return index; + } + + let closestIndex = -1; + let closestValue = 100000; + + for ( let i = 0; i < list.length; i++ ) + { + let listValue = getText( list[ i ] ); + let distance = LevenshteinDistance.compute( listValue, matching ); + + if ( distance >= closestValue ) + { + continue; + } + + closestValue = distance; + closestIndex = i; + + } + + return closestIndex; + + } + + static makeGlobal( regexp:RegExp ) + { + if ( regexp.global ) + { + return regexp; + } + + var source = regexp.source; + var flags = regexp.flags + "g"; + + return new RegExp( source, flags ); + } + + static makeIgnoreCase( regexp:RegExp ) + { + if ( regexp.ignoreCase ) + { + return regexp; + } + + var source = regexp.source; + var flags = regexp.flags + "i"; + + return new RegExp( source, flags ); + } + + static prependZeros( source:string, minimumLength:number = 2 ) + { + while ( source.length < minimumLength ) + { + source = "0" + source; + } + + return source; + } + + static createWordMatcher( source:string ) + { + source = "\\b" + RegExpUtility.toRegexSource( source ) + "\\b"; + return new RegExp( source ); + } + + static createMatcher( source:string ) + { + return new RegExp( RegExpUtility.toRegexSource( source ) ); + } + + + static createRegExp( regexp:RegExp, matching:string, replacement:string ) + { + + let source = regexp.source; + let flags = regexp.flags; + source = source.replace( matching, replacement ); + + return new RegExp( source, flags ); + } + + static readonly ES_NUMBER_REGEX = /((\d+)?\.)?\d+(e(\+|\-)\d+)?/; + + static isESNumber( value:string ) + { + return RegExpUtility.ES_NUMBER_REGEX.test( value ); + } + + static parentPath( path:string, alternative:string ="") + { + let lastSlash = path.lastIndexOf( "/" ); + let lastBackSlash = path.lastIndexOf( "\\" ) + + if ( lastSlash === -1 && lastBackSlash === -1 ) + { + return alternative; + } + + let highest = Math.max( lastSlash, lastBackSlash ); + + return path.substring( 0, highest ); + } + + + static fileNameWithoutExtension( path:string ) + { + return RegExpUtility.trimFileTypeExtension( RegExpUtility.fileNameOrLastPath( path ) ); + } + + static fileNameOrLastPath( path:string ) + { + path = this.normalizePath( path ); + let lastSlash = path.lastIndexOf( "/" ); + + if ( lastSlash === -1 ) + { + return path; + } + + return path.substring( lastSlash + 1 ); + + /* + path = path.replace( /(\\|\/)$/, "" ); + let parentPath = RegExpUtility.parentPath( path ); + + if ( parentPath === "" ) + { + path = path.replace( /^(\/|\\)/, "" ); + return path; + } + + let last = path.substring( parentPath.length + 1 ); + + last = last.replace( /^(\/|\\)/, "" ); + return last; + */ + } + + static join( pathA:string, pathB:string, ...paths:string[] ) + { + let normalizedPaths = [pathA,pathB].concat( paths ); + + for ( let i = 0; i < normalizedPaths.length; i++ ) + { + normalizedPaths[ i ] = this.normalizePath( normalizedPaths[ i ] ); + } + + return normalizedPaths.join( "/" ); + } + + static joinPaths( paths:string[] ) + { + let normalizedPaths = paths; + + for ( let i = normalizedPaths.length - 1; i >= 0; i-- ) + { + if ( this.isEmptyPath( normalizedPaths[ i ] ) ) + { + Arrays.removeAt( normalizedPaths, i ); + } + } + + for ( let i = 0; i < normalizedPaths.length; i++ ) + { + normalizedPaths[ i ] = this.normalizePath( normalizedPaths[ i ] ); + } + + return normalizedPaths.join( "/" ); + } + + static normalizePath( path:string ) + { + let slashMatcher = /\\/g; + let multiples = /\/\/+/g; + let startSlashes = /^\/+/; + let endSlashes = /\/+$/; + + + path = path.replace( slashMatcher, "/" ); + path = path.replace( multiples, "/" ); + path = path.replace( startSlashes, "" ); + path = path.replace( endSlashes, "" ); + + + return path; + } + + +} diff --git a/browser/text/lexer/CLikeLexer.ts b/browser/text/lexer/CLikeLexer.ts new file mode 100644 index 0000000..c4ba7c7 --- /dev/null +++ b/browser/text/lexer/CLikeLexer.ts @@ -0,0 +1,34 @@ +import { Lexer } from "./Lexer"; +import { LexerMatcher } from "./LexerMatcher"; +import { LexerMatcherLibrary } from "./LexerMatcherLibrary"; +import { LexerType } from "./LexerType"; + +export class CLikeLexer extends Lexer +{ + constructor() + { + super(); + + this.addAllMatchers( + LexerMatcherLibrary.SINGLE_LINE_COMMENT_MATCHER, + LexerMatcherLibrary.MULTI_LINE_COMMENT_MATCHER, + LexerMatcherLibrary.DOUBLE_QUOTED_STRING_MATCHER, + LexerMatcherLibrary.SINGLE_QUOTED_STRING_MATCHER, + LexerMatcherLibrary.C_INSTRUCTION_MATCHER, + LexerMatcherLibrary.NUMBER_MATCHER, + LexerMatcherLibrary.NULL_MATCHER, + LexerMatcherLibrary.BOOL_MATCHER, + LexerMatcherLibrary.BREAK_MATCHER, + LexerMatcherLibrary.WHITESPACE_MATCHER, + LexerMatcherLibrary.LOGIC_MATCHER, + LexerMatcherLibrary.BRACKET_MATCHER, + LexerMatcherLibrary.ACCESS_MODIFIER_MATCHER, + LexerMatcherLibrary.CLASS_MATCHER, + LexerMatcherLibrary.OPERATOR_MATCHER, + LexerMatcherLibrary.CFUNCTION_MATCHER, + LexerMatcherLibrary.CWORD_MATCHER, + LexerMatcherLibrary.ANY_SYMBOL_MATCHER + ); + + } +} \ No newline at end of file diff --git a/browser/text/lexer/CSVLexer.ts b/browser/text/lexer/CSVLexer.ts new file mode 100644 index 0000000..1a84180 --- /dev/null +++ b/browser/text/lexer/CSVLexer.ts @@ -0,0 +1,28 @@ + +import { Lexer } from "./Lexer"; +import { LexerMatcher } from "./LexerMatcher"; +import { LexerMatcherLibrary } from "./LexerMatcherLibrary"; +import { LexerType } from "./LexerType"; + + +export class CSVLexer extends Lexer +{ + static readonly COMMA = "COMMA"; + static readonly COMMA_MATCHER = new LexerMatcher( CSVLexer.COMMA, /\,/ ); + + static readonly DATA = "DATA"; + static readonly DATA_MATCHER = new LexerMatcher( CSVLexer.DATA, /(\w|\d|\s)+/ ); + + constructor() + { + super(); + + this.addAllMatchers( + LexerMatcherLibrary.DOUBLE_QUOTED_STRING_MATCHER, + CSVLexer.COMMA_MATCHER, + LexerMatcherLibrary.BREAK_MATCHER, + LexerMatcherLibrary.ANY_SYMBOL_MATCHER + ); + + } +} \ No newline at end of file diff --git a/browser/text/lexer/HighlightedHTML.ts b/browser/text/lexer/HighlightedHTML.ts new file mode 100644 index 0000000..fa2d298 --- /dev/null +++ b/browser/text/lexer/HighlightedHTML.ts @@ -0,0 +1,99 @@ +import { LexerEvent } from "./LexerEvent"; +import { Lexer } from "./Lexer"; +import { LexerQuery } from "./LexerQuery"; +import { ElementType } from "../../dom/ElementType"; +import { ElementAttribute } from "../../dom/ElementAttribute"; + +export class HighlightedHTML +{ + static readonly codeToken = new ElementType( "code-token" ); + static readonly type = new ElementAttribute( "type" ); + + static createLexerStyles( lexer:Lexer, prefixSelector:string = "", styles:string="" ) + { + let matchers = lexer.matchers; + + for ( let i = 0; i < matchers.length; i++ ) + { + let matcher = matchers[ i ]; + let hue = 360 - i / matchers.length * 360; + + let style = `\n${prefixSelector}code-token-${matcher.type}{ color: hsl(${hue}, 60%, 60%)}`; + + styles += style; + } + + return styles; + } + + static tokenize( query:LexerQuery ) + { + let htmlTags = query.tokens.map( + t=> + { + if ( t.isDone || t.isError ) + { + return ""; + } + + let rawValue = t.getMatch( query.source ); + let textNode = document.createTextNode( rawValue ); + let wrapper = document.createElement( "div" ); + wrapper.appendChild( textNode ); + let escapedText = wrapper.innerHTML; + return `${escapedText}` + } + ); + + return htmlTags.join( "" ); + } + + static auto( lexer:Lexer, query:LexerQuery ) + { + let tokens = query.tokens; + let source = query.source; + + + + let htmlTags = tokens.map( + t=> + { + let rawValue = t.getMatch( source ); + let textNode = document.createTextNode( rawValue ); + let wrapper = document.createElement( "div" ); + wrapper.appendChild( textNode ); + let escapedText = wrapper.innerHTML; + return `${escapedText}` + } + ); + + let styles = + ` + body + { + background-color: hsl(0,0%,10%); + } + + pre + { + font-family: Ubuntu Mono; + } + `; + + let matchers = lexer.matchers; + + for ( let i = 0; i < matchers.length; i++ ) + { + let matcher = matchers[ i ]; + let hue = i / matchers.length * 360; + + let style = `\ncode-token[data-type="${matcher.type}"]{ color: hsl(${hue}, 60%, 60%)}`; + + styles += style; + } + + let html = `
${htmlTags.join( "" )}
` + + return html; + } +} \ No newline at end of file diff --git a/browser/text/lexer/Lexer.ts b/browser/text/lexer/Lexer.ts new file mode 100644 index 0000000..87edcb3 --- /dev/null +++ b/browser/text/lexer/Lexer.ts @@ -0,0 +1,170 @@ +import { LexerMatcher } from "./LexerMatcher"; +import { LexerEvent } from "./LexerEvent"; + +export class Lexer +{ + static readonly defaultMode = "default"; + private _modes = new Map(); + + get modes() + { + return this._modes; + } + + get matchers():LexerMatcher[] + { + let output:LexerMatcher[] = []; + + for ( let mode of this._modes ) + { + let matchers = mode[ 1 ]; + + for ( let matcher of matchers ) + { + output.push( matcher ); + } + + } + + return output; + } + + addMatcher( matcher:LexerMatcher ) + { + let list:LexerMatcher[] = this._modes.get( matcher.mode ); + + if ( ! list ) + { + list = []; + this._modes.set( matcher.mode, list ); + } + + list.push( matcher ); + } + + resolveMatches( source:string, events:LexerEvent[] ) + { + events.forEach( e => e.getMatch( source ) ); + } + + addAllMatchers( ...matchers:LexerMatcher[] ) + { + for ( let m of matchers ) + { + this.addMatcher( m ); + } + + } + + public lex( source:string, callback:( e:LexerEvent) => void, offset:number = 0, mode:string = "" ) + { + + let lexerEvent = new LexerEvent( "", 0, -2 ); + let lastMatcher = null; + + while ( offset < source.length ) + { + if ( ! this._modes.has( mode ) ) + { + let errorMessage = "@Lexer-Error. Mode not found: '" + mode + "'"; + console.log( errorMessage, "@", offset ); + lexerEvent.set( errorMessage, offset, LexerEvent.LENGTH_ERROR_ID ); + + callback( lexerEvent ); + + return; + } + + let matchers = this._modes.get( mode ); + let foundSomething = false; + + for ( let i = 0; i < matchers.length; i++ ) + { + let matcher = matchers[ i ]; + let matchLength = matcher.matchLength( source, offset ); + + if ( matchLength > 0 ) + { + lexerEvent.set( matcher.type, offset, matchLength ); + callback( lexerEvent ); + + foundSomething = true; + + i = matchers.length; + + if ( matcher.nextMode ) + { + mode = matcher.nextMode; + } + + offset += matchLength; + + } + } + + if ( ! foundSomething ) + { + let errorMessage = "@Lexer-Error. No match: '" + mode + "'"; + console.log( errorMessage, "@", offset ); + lexerEvent.set( errorMessage, offset, LexerEvent.LENGTH_ERROR_ID ); + + callback( lexerEvent ); + + return; + } + + } + + lexerEvent.set( mode, offset, LexerEvent.LENGTH_DONE_ID ); + callback( lexerEvent ); + } + + lexTokens( source:string, offset:number = 0, mode:string = Lexer.defaultMode ) + { + let events = this.lexToList( source, offset, mode ); + + this.resolveMatches( source, events ); + + return events; + } + + lexToList( source:string, offset:number = 0, mode:string = Lexer.defaultMode ) + { + var list:LexerEvent[] = []; + + this.lex( source, + ( token:LexerEvent )=> + { + list.push( token.clone() ); + }, + offset, mode + ); + + return list; + } + + compress( tokens:LexerEvent[], compressTypes:Set ) + { + let lastToken:LexerEvent = null; + let compressedTokens:LexerEvent[] = []; + + tokens.forEach( + t => + { + if ( lastToken && t.type === lastToken.type && compressTypes.has( t.type ) ) + { + lastToken.extendLength( t.length ); + } + else + { + lastToken = t.clone(); + compressedTokens.push( lastToken ); + } + } + ); + + return compressedTokens; + } + + +} \ No newline at end of file diff --git a/browser/text/lexer/LexerEvent.ts b/browser/text/lexer/LexerEvent.ts new file mode 100644 index 0000000..e312234 --- /dev/null +++ b/browser/text/lexer/LexerEvent.ts @@ -0,0 +1,80 @@ +export class LexerEvent +{ + static readonly LENGTH_ERROR_ID = -1; + static readonly LENGTH_DONE_ID = -2; + + private _type:string; + get type(){ return this._type; } + private _offset:number; + get offset(){ return this._offset; } + private _length:number; + get length(){ return this._length; } + private _match:string = null; + get match(){ return this._match;} + + get exclusiveEnd() + { + return this.offset + this.length; + } + + get lastSourceIndex() + { + return this.exclusiveEnd - 1; + } + + extendLength( num:number ) + { + this._length += num; + } + + constructor( type:string, offset:number, length:number ) + { + this.set( type, offset, length ); + } + + set( type:string, offset:number, length:number ) + { + this._type = type; + this._offset = offset; + this._length = length; + } + + isType( type:string ) + { + return this._type == type; + } + + get isError(){ return this._length === LexerEvent.LENGTH_ERROR_ID; } + get isDone(){ return this._length === LexerEvent.LENGTH_DONE_ID; } + get isDoneOrError(){ return this.isDone || this.isError } + + get end() { return this._offset + this._length; } + + toString() + { + return "Token{ '" + this._type + "' (" + this._offset + "-" + this.end + ") }"; + } + + clone() + { + return new LexerEvent( this._type, this._offset, this._length ); + } + + + getMatch( source:string ) + { + if ( this._match ) + { + return this._match; + } + + this.setMatch( source.substring( this._offset, this.end ) ); + return this._match; + } + + setMatch( source:string ) + { + this._match = source; + } + +} \ No newline at end of file diff --git a/browser/text/lexer/LexerEventMatcher.ts b/browser/text/lexer/LexerEventMatcher.ts new file mode 100644 index 0000000..5e99dbd --- /dev/null +++ b/browser/text/lexer/LexerEventMatcher.ts @@ -0,0 +1,94 @@ +import { BooleanExpression } from "../../expressions/BooleanExpression"; +import { StringValueMatcher, LitaralMatcher, RegExpMatcher } from "../../expressions/StringMatcher"; +import { Arrays } from "../../tools/Arrays"; +import { LexerEvent } from "./LexerEvent"; +import { LexerSequence } from "./LexerSequence"; +import { LexerType } from "./LexerType"; + +export class LiteralLexerEventTypeMatcher extends LitaralMatcher +{ + getStringValue( l:LexerEvent){return l.type;} +} + +export class RegExpLexerEventTypeMatcher extends RegExpMatcher +{ + getStringValue( l:LexerEvent){return l.type;} +} + +export class LiteralLexerEventValueMatcher extends LitaralMatcher +{ + getStringValue( l:LexerEvent){return l.match;} +} + +export class RegExpLexerEventValueMatcher extends RegExpMatcher +{ + getStringValue( l:LexerEvent){return l.match;} +} + +export class LexerEventMatcherTools +{ + static createTypeAndValueMatcher( type:Type, value:string ) + { + let typeMatcher = new LiteralLexerEventTypeMatcher( type ); + let valueMatcher = new LiteralLexerEventValueMatcher( value ); + + return typeMatcher.AND( valueMatcher ); + } + + static convertToExpression( value:BooleanExpression|Type[]|string ):BooleanExpression + { + if ( typeof value === "string" ) + { + return new LiteralLexerEventValueMatcher( value ) + } + else if ( Array.isArray( value ) ) + { + let types = value as LexerType[]; + let outputMatcher:BooleanExpression = null; + + for ( let i = 0; i < types.length; i++ ) + { + let matcher = new LiteralLexerEventTypeMatcher( types[ i ] ); + + if ( outputMatcher ) + { + outputMatcher = outputMatcher.OR( matcher ); + } + else + { + outputMatcher = matcher; + } + } + + return outputMatcher; + } + + return value as BooleanExpression; + } + + static createMatchSequence( matcherDefinition:(BooleanExpression|Type[]|string)[], + ignoreDefinition:(BooleanExpression|Type[]|string)[] ) + { + let matchers = matcherDefinition.map( md => LexerEventMatcherTools.convertToExpression( md ) ); + let ignores = ignoreDefinition.map( id => LexerEventMatcherTools.convertToExpression( id ) ); + + + let ignore = null; + + if ( ignores.length === 1 ) + { + ignore = ignores[ 0 ]; + } + else if ( ignores.length > 1 ) + { + ignore = ignores[ 0 ].OR_ANY( Arrays.copyRangeFromStart( ignores, 1 ) ); + } + + let sequence = new LexerSequence(); + sequence.sequence = matchers; + sequence.ignore = ignore; + + return sequence; + } +} + diff --git a/browser/text/lexer/LexerMatcher.ts b/browser/text/lexer/LexerMatcher.ts new file mode 100644 index 0000000..55c6966 --- /dev/null +++ b/browser/text/lexer/LexerMatcher.ts @@ -0,0 +1,85 @@ + +import { RegExpUtility } from "../RegExpUtitlity"; +import { Lexer } from "./Lexer"; + +export class LexerMatcher +{ + _mode:string; + _type:string; + _nextMode:string; + _matcher:RegExp; + compressTokens:boolean = false; + + get mode(){ return this._mode; } + get type(){ return this._type; } + get nextMode(){ return this._nextMode ? this._nextMode : this._mode; } + get matcher() { return this._matcher; } + + constructor( type:string, matcher:RegExp, mode:string = Lexer.defaultMode, nextMode:string=null ) + { + this._type = type; + this._matcher = RegExpUtility.makeSticky( matcher ); + this._mode = mode; + this._nextMode = nextMode; + } + + getMatch( value:string, index:number = 0, alternative:string = null) + { + let result = this._matcher.exec( value ); + + if ( ! result || result.length <= index ) + { + return alternative; + } + + return result[ index ]; + } + + matchLength( source:string, offset:number ) + { + this._matcher.lastIndex = offset; + var result = this._matcher.exec( source ); + + if ( result && result.index != offset ) + { + var message = "Illegal match index! Not a sticky matcher: " + this._matcher + "."; + message += "Offset: " + offset + " Matched Index: " + result.index ; + throw message; + } + + if ( result ) + { + return result[ 0 ].length; + } + + return -1; + } + + + getMatchResult( source:string, offset:number ) + { + this._matcher.lastIndex = offset; + var result = this._matcher.exec( source ); + + if ( result && result.index != offset ) + { + var message = "Illegal match index! Not a sticky matcher: " + this._matcher + "."; + message += "Offset: " + offset + " Matched Index: " + result.index ; + throw message; + } + + return result; + } + + isMatching( source:string ) + { + let matchLength = this.matchLength( source, 0 ); + + if ( matchLength === source.length ) + { + return true; + } + + return false; + } +} \ No newline at end of file diff --git a/browser/text/lexer/LexerMatcherLibrary.ts b/browser/text/lexer/LexerMatcherLibrary.ts new file mode 100644 index 0000000..642eebc --- /dev/null +++ b/browser/text/lexer/LexerMatcherLibrary.ts @@ -0,0 +1,95 @@ +import { LexerTypes } from "./LexerType"; +import { LexerMatcher } from "./LexerMatcher"; + +export class LexerMatcherLibrary +{ + static readonly CWORD_MATCHER = + new LexerMatcher( LexerTypes.CWORD, /[a-zA-Z_]\w*/ ); + + static readonly CFUNCTION_MATCHER = + new LexerMatcher( LexerTypes.CFUNCTION, /[a-zA-Z_]\w*(?=\s*\()/ ); + +// static readonly CLASSNAME_MATCHER = +// new LexerMatcher( LexerTypes.CLASSNAME, /(?<=(?:(?:class|interface|struct)\s+))[a-zA-Z_]\w*/ ); + + static readonly JSWORD_MATCHER = + new LexerMatcher( LexerTypes.JSWORD, /[a-zA-Z_\$]\w*/ ); + + static readonly PHPWORD_MATCHER = + new LexerMatcher( LexerTypes.PHPWORD, /\$?[a-zA-Z_]\w*/i ); + + + static readonly CSS_CLASS_SELECTOR_MATCHER = + new LexerMatcher( LexerTypes.CSS_CLASS_SELECTOR, /\.[a-zA-Z_](\w|\-)*/ ); + + static readonly CSS_ID_SELECTOR_MATCHER = + new LexerMatcher( LexerTypes.CSS_ID_SELECTOR, /\#[a-zA-Z_](\w|\-)*/ ); + + static readonly CSS_WORD_MATCHER = + new LexerMatcher( LexerTypes.CSS_WORD, /[a-zA-Z_](\w|\-)*/ ); + + + static readonly HTML_CUSTOM_ELEMENT_MATCHER = + new LexerMatcher( LexerTypes.HTML_CUSTOM_ELEMENT, /[a-zA-Z]\w*\-(\w|\-)+/ ); + + static readonly DOUBLE_QUOTED_STRING_MATCHER = + new LexerMatcher( LexerTypes.DOUBLE_QUOTED_STRING, /"(?:[^"\\]|\\.)*"/ ); + + static readonly SINGLE_QUOTED_STRING_MATCHER = + new LexerMatcher( LexerTypes.SINGLE_QUOTED_STRING, /'(?:[^'\\]|\\.)*'/ ); + + static readonly NUMBER_MATCHER = + new LexerMatcher( LexerTypes.NUMBER, /(?=\.\d|\d)(?:\d+)?(?:\.?\d*)(?:[eE][+-]?\d+)?/ ); + + static readonly WHITESPACE_MATCHER = + new LexerMatcher( LexerTypes.WHITESPACE, /\s+/ ); + + static readonly BREAK_MATCHER = + new LexerMatcher( LexerTypes.BREAK, /(\r\n|\r|\n)/ ); + + + static readonly NULL_MATCHER = + new LexerMatcher( LexerTypes.NULL, /null/ ); + + static readonly BOOL_MATCHER = + new LexerMatcher( LexerTypes.BOOL, /true|false/ ); + + static readonly LOGIC_MATCHER = + new LexerMatcher( LexerTypes.LOGIC, /if|else|switch|do|while|for|break|continue|return/ ); + + static readonly OPERATOR_MATCHER = + new LexerMatcher( LexerTypes.OPERATOR, /(?:\=\=)|(?:\+\+)|(?:\-\-)|\+|\-|\*|\/|\^|\||\~|\&|\%|\<|\>|\=|\!|\.|\:|\,|\;/ ); + + static readonly BRACKET_MATCHER = + new LexerMatcher( LexerTypes.BRACKET, /\(|\)|\[|\]|\{|\}/ ); + + static readonly BLOCKSTART_MATCHER = + new LexerMatcher( LexerTypes.BLOCKSTART, /\{/ ); + + static readonly BLOCKEND_MATCHER = + new LexerMatcher( LexerTypes.BLOCKEND, /\}/ ); + + static readonly CLASS_MATCHER = + new LexerMatcher( LexerTypes.CLASS, /\bclass\b/ ); + + static readonly ACCESS_MODIFIER_MATCHER = + new LexerMatcher( LexerTypes.ACCESS_MODIFIER, /\b(?:public|protected|private)\b/ ); + + static readonly SINGLE_LINE_COMMENT_MATCHER = + new LexerMatcher( LexerTypes.SINGLE_LINE_COMMENT, /\/\/.*/ ); + + static readonly C_INSTRUCTION_MATCHER = + new LexerMatcher( LexerTypes.C_INSTRUCTION, /\#.*/ ); + + static readonly MULTI_LINE_COMMENT_MATCHER = + new LexerMatcher( LexerTypes.MULTI_LINE_COMMENT, /\/\*(.|(\r\n|\r|\n))*?\*\// ); + + static readonly ANY_SYMBOL_MATCHER = + new LexerMatcher( LexerTypes.ANY_SYMBOL, /./ ); + + static readonly HASH_TAG = + new LexerMatcher( LexerTypes.HASH_TAG, /\#(\w|-|\d)+/ ); + + static readonly URL = + new LexerMatcher( LexerTypes.URL, /https?\:\/\/(\w|\.|\-|\?|\=|\+|\/)+/ ); +} \ No newline at end of file diff --git a/browser/text/lexer/LexerMatcherLibraryTest.ts b/browser/text/lexer/LexerMatcherLibraryTest.ts new file mode 100644 index 0000000..37e6e85 --- /dev/null +++ b/browser/text/lexer/LexerMatcherLibraryTest.ts @@ -0,0 +1,91 @@ +import { LexerTypes } from "./LexerType"; +import { LexerMatcher } from "./LexerMatcher"; + +export class LexerMatcherLibraryTest +{ + static readonly CWORD_MATCHER = + new LexerMatcher( LexerTypes.CWORD, /[a-zA-Z_]\w*/ ); + + static readonly CFUNCTION_MATCHER = + new LexerMatcher( LexerTypes.CFUNCTION, /[a-zA-Z_]\w*(?=\s*\()/ ); + + + // static readonly CLASSNAME_MATCHER = + // new LexerMatcher( LexerTypes.CLASSNAME, /(?<=(?:(?:class|interface|struct)\s+))[a-zA-Z_]\w*/ ); + + static readonly JSWORD_MATCHER = + new LexerMatcher( LexerTypes.JSWORD, /[a-zA-Z_\$]\w*/ ); + + static readonly PHPWORD_MATCHER = + new LexerMatcher( LexerTypes.PHPWORD, /\$?[a-zA-Z_]\w*/ ); + + + static readonly CSS_CLASS_SELECTOR_MATCHER = + new LexerMatcher( LexerTypes.CSS_CLASS_SELECTOR, /\.[a-zA-Z_](\w|\-)*/ ); + + static readonly CSS_ID_SELECTOR_MATCHER = + new LexerMatcher( LexerTypes.CSS_ID_SELECTOR, /\#[a-zA-Z_](\w|\-)*/ ); + + static readonly CSS_WORD_MATCHER = + new LexerMatcher( LexerTypes.CSS_WORD, /[a-zA-Z_](\w|\-)*/ ); + + + static readonly HTML_CUSTOM_ELEMENT_MATCHER = + new LexerMatcher( LexerTypes.HTML_CUSTOM_ELEMENT, /[a-zA-Z]\w*\-(\w|\-)+/ ); + + static readonly DOUBLE_QUOTED_STRING_MATCHER = + new LexerMatcher( LexerTypes.DOUBLE_QUOTED_STRING, /"(?:[^"\\]|\\.)*"/ ); + + static readonly SINGLE_QUOTED_STRING_MATCHER = + new LexerMatcher( LexerTypes.SINGLE_QUOTED_STRING, /'(?:[^'\\]|\\.)*'/ ); + + static readonly NUMBER_MATCHER = + new LexerMatcher( LexerTypes.NUMBER, /(?=\.\d|\d)(?:\d+)?(?:\.?\d*)(?:[eE][+-]?\d+)?/ ); + + static readonly WHITESPACE_MATCHER = + new LexerMatcher( LexerTypes.WHITESPACE, /\s+/ ); + + static readonly BREAK_MATCHER = + new LexerMatcher( LexerTypes.BREAK, /\n/ ); + + static readonly NULL_MATCHER = + new LexerMatcher( LexerTypes.NULL, /null/ ); + + static readonly BOOL_MATCHER = + new LexerMatcher( LexerTypes.BOOL, /true|false/ ); + + static readonly LOGIC_MATCHER = + new LexerMatcher( LexerTypes.LOGIC, /if|else|switch|do|while|for|break|continue|return/ ); + + static readonly OPERATOR_MATCHER = + new LexerMatcher( LexerTypes.OPERATOR, /\+|\-|\*|\/|\^|\||\~|\&|\%|\<|\>|\=|\!|\.|\:|\,|\;/ ); + + static readonly BRACKET_MATCHER = + new LexerMatcher( LexerTypes.BRACKET, /\(|\)|\[|\]|\{|\}/ ); + + static readonly BLOCKSTART_MATCHER = + new LexerMatcher( LexerTypes.BLOCKSTART, /\{/ ); + + static readonly BLOCKEND_MATCHER = + new LexerMatcher( LexerTypes.BLOCKEND, /\}/ ); + + static readonly CLASS_MATCHER = + new LexerMatcher( LexerTypes.CLASS, /class/ ); + + static readonly ACCESS_MODIFIER_MATCHER = + new LexerMatcher( LexerTypes.ACCESS_MODIFIER, /public|protected|private/ ); + + static readonly SINGLE_LINE_COMMENT_MATCHER = + new LexerMatcher( LexerTypes.SINGLE_LINE_COMMENT, /\/\/.*/ ); + + static readonly C_INSTRUCTION_MATCHER = + new LexerMatcher( LexerTypes.C_INSTRUCTION, /\#.*/ ); + + static readonly MULTI_LINE_COMMENT_MATCHER = + new LexerMatcher( LexerTypes.MULTI_LINE_COMMENT, /\/\*(.|(\r\n|\r|\n))*?\*\// ); + + static readonly ANY_SYMBOL_MATCHER = + new LexerMatcher( LexerTypes.ANY_SYMBOL, /./ ); + + +} \ No newline at end of file diff --git a/browser/text/lexer/LexerQuery.ts b/browser/text/lexer/LexerQuery.ts new file mode 100644 index 0000000..d9c8592 --- /dev/null +++ b/browser/text/lexer/LexerQuery.ts @@ -0,0 +1,261 @@ +import { BooleanExpression } from "../../expressions/BooleanExpression"; +import { LexerEvent } from "./LexerEvent"; + + +export class LexerQuery +{ + source:string; + tokens:LexerEvent[]; + + _index:Map = new Map(); + + createTokenIndex() + { + for ( let i = 0; i < this.tokens.length; i++ ) + { + this.tokens[ i ].getMatch( this.source ); + this._index.set( this.tokens[ i ], i ); + } + } + + createReplacedOutput( replacements:Map ) + { + let output = []; + + for ( let i = 0; i < this.tokens.length; i++ ) + { + let token = this.tokens[ i ]; + + if ( token.isDone || token.isError ) + { + continue; + } + + if ( replacements.has( token ) ) + { + output.push( replacements.get( token ) ); + } + else + { + output.push( token.match ); + } + } + + return output.join( "" ); + } + + linePosition( offset:number ) + { + let lineIndex = 1; + let lastLineBreak = 0; + + for ( let i = 0; i < offset; i++ ) + { + if ( this.source[ i ] === "\n" || this.source === "\r" ) + { + if ( this.source[ i ] === "\n" && this.source[ i + 1 ] === "\r" ) + { + i++ + } + + lineIndex++; + lastLineBreak = i; + } + } + + let characterIndex = offset - lastLineBreak; + + return { line: lineIndex, characterIndex: characterIndex }; + + } + + lineInfo( l:LexerEvent, breaksBefore:number=2, breaksAfter:number=2 ) + { + let startIndex = l.offset - 1; + let endIndex = l.offset + 1; + + let numLineBreakBeforeFound = 0; + + for ( let i = startIndex; i >= 0 && numLineBreakBeforeFound < breaksBefore; i-- ) + { + if ( this.source[ i ] === "\n" || this.source[ i ] === "\r" ) + { + if ( this.source[ i ] === "\n" && i > 0 && this.source[ i - 1 ] === "\r" ) + { + i--; + } + + numLineBreakBeforeFound ++; + + } + else + { + startIndex = i; + } + } + + + let numLineBreakAfterFound = 0; + + for ( let i = startIndex; i >= 0 && numLineBreakAfterFound < breaksAfter; i++ ) + { + if ( this.source[ i ] === "\n" || this.source[ i ] === "\r" ) + { + if ( this.source[ i ] === "\r" && i < ( this.source.length - 1 ) && this.source[ i + 1 ] === "\n" ) + { + i++; + } + + numLineBreakAfterFound ++; + + } + else + { + endIndex = i; + } + } + + let info = this.source.substring( startIndex, endIndex ); + + return info; + } + + index( l:LexerEvent ) + { + return this._index.get( l ); + } + + all( matcher:BooleanExpression ):LexerEvent[] + { + let output:LexerEvent[] = []; + + for ( let i = 0; i < this.tokens.length; i++ ) + { + if ( matcher.evaluate( this.tokens[ i ] ) ) + { + output.push( this.tokens[ i ] ); + } + } + + return output; + } + + forAllMatches( index:number, end:number, callback:(s:string)=>void) + { + for ( let i = index; i <= end; i++ ) + { + callback( this.tokens[ i ].match ); + } + } + + forAllWithType( type:string, callback:( t:LexerEvent )=>void ) + { + for ( let i =0; i < this.tokens.length; i++ ) + { + if ( ! this.tokens[ i ].isType( type ) ) + { + continue; + } + + callback( this.tokens[ i ] ); + } + } + + searchBlockIndices( index:number, start:BooleanExpression, end:BooleanExpression) + { + let startIndex = -1; + + let numOpen = 0; + let blocksFound = false; + + for ( let i = index; i < this.tokens.length; i++ ) + { + let token = this.tokens[ i ]; + + if ( start.evaluate( token ) ) + { + if ( ! blocksFound ) + { + startIndex = i; + blocksFound = true; + } + + numOpen ++; + } + + if ( end.evaluate( token ) ) + { + numOpen --; + + if ( numOpen < 0 ) + { + return null; + } + + if ( numOpen === 0 ) + { + return { startIndex, endIndex: i, length: ( 1 + i - startIndex ) }; + } + } + } + + return null; + } + + searchIndex( index:number, forward:boolean, matcher:BooleanExpression, ignore?:BooleanExpression, maxItems:number=100000 ) + { + let itemCount = 0; + + if ( forward ) + { + let next = index + 1; + + for ( let i = next; i < this.tokens.length && itemCount < maxItems; i++, itemCount++ ) + { + let token = this.tokens[ i ]; + + if ( ignore && ignore.evaluate( token ) ) + { + continue; + } + + if ( matcher.evaluate( token ) ) + { + return i; + } + + itemCount++; + } + } + else + { + let previous = index - 1; + + for ( let i = previous; i >=0 && itemCount < maxItems; i--, itemCount++ ) + { + let token = this.tokens[ i ]; + + if ( ignore && ignore.evaluate( token ) ) + { + continue; + } + + if ( matcher.evaluate( token ) ) + { + return i; + } + + itemCount++; + } + } + + return -1; + } + + searchItem( index:number, forward:boolean, matcher:BooleanExpression, ignore?:BooleanExpression, maxItems:number = 100000 ) + { + let nextIndex = this.searchIndex( index, forward, matcher, ignore ); + + return nextIndex === -1 ? null : this.tokens[ nextIndex ]; + } +} \ No newline at end of file diff --git a/browser/text/lexer/LexerSequence.ts b/browser/text/lexer/LexerSequence.ts new file mode 100644 index 0000000..7d4d611 --- /dev/null +++ b/browser/text/lexer/LexerSequence.ts @@ -0,0 +1,88 @@ +import { LexerEvent } from "./LexerEvent"; +import { BooleanExpression } from "../../expressions/BooleanExpression"; +import { LexerQuery } from "./LexerQuery"; + +export class LexerSequence +{ + sequence:BooleanExpression[] = []; + ignore:BooleanExpression = null; + + + get( query:LexerQuery, tokenOffset:number, sequenceOffset:number ):number[] + { + let sequenceIndices:number[] = []; + + for ( let s of this.sequence ){ sequenceIndices.push( -1 ); } + + if ( ! this.sequence[ sequenceOffset ].evaluate( query.tokens[ tokenOffset ] ) ) + { + //console.log( "Offset is not matching" ); + return null; + } + + sequenceIndices[ sequenceOffset ] = tokenOffset; + + let previousSearchIndex = tokenOffset; + + for ( let i = sequenceOffset - 1; i >= 0; i-- ) + { + let matcher = this.sequence[ i ]; + let index = query.searchIndex( previousSearchIndex, false, matcher, this.ignore, 2 ); + + if ( index === -1 ) + { + let startTokenIndex = Math.max( 0, previousSearchIndex - 5 ); + let previousTokens = query.tokens.slice( startTokenIndex, previousSearchIndex ); + let infos = previousTokens.map( + ( le )=> + { + let ignoreResult = this.ignore.evaluate( le ); + let matchResult = matcher.evaluate( le ); + return `'${le.match}' ${query.index(le)} ${le.type} ignore:${ignoreResult} match:${matchResult}`; + } + ); + + //console.log( `Sequence not matching at #${i}(${previousSearchIndex})`, matcher, "\n", `[${infos}]` ); + return null; + } + + sequenceIndices[ i ] = index; + previousSearchIndex = index; + let token = query.tokens[ index ]; + + } + + let nextSearchIndex = tokenOffset; + + for ( let i = sequenceOffset + 1; i < this.sequence.length; i++ ) + { + let matcher = this.sequence[ i ]; + let index = query.searchIndex( nextSearchIndex, true, matcher, this.ignore, 2 ); + + if ( index === -1 ) + { + let endTokenIndex = Math.min( query.tokens.length, nextSearchIndex + 5 ); + let nextTokens = query.tokens.slice( nextSearchIndex, endTokenIndex ); + + let infos = nextTokens.map( + ( le )=> + { + let ignoreResult = this.ignore.evaluate( le ); + let matchResult = matcher.evaluate( le ); + return `'${le.match}' ${query.index(le)} ${le.type} ignore:${ignoreResult} match:${matchResult}`; + } + ); + + //console.log( `Sequence not at #${i}(${nextSearchIndex})`, matcher, "\n", `[${infos}]` ); + return null; + } + + sequenceIndices[ i ] = index; + nextSearchIndex = index; + let token = query.tokens[ index ]; + + } + + return sequenceIndices; + } +} \ No newline at end of file diff --git a/browser/text/lexer/LexerType.ts b/browser/text/lexer/LexerType.ts new file mode 100644 index 0000000..a0fdb6f --- /dev/null +++ b/browser/text/lexer/LexerType.ts @@ -0,0 +1,143 @@ +import { Lexer } from "./Lexer"; + +export type CWORD = "CWORD"; +export type JSWORD = "JSWORD"; +export type PHPWORD = "PHPWORD"; +export type CFUNCTION = "CFUNCTION"; +export type CLASSNAME = "CLASSNAME"; + +export type CSS_ID_SELECTOR = "CSS_ID_SELECTOR"; +export type CSS_CLASS_SELECTOR = "CSS_CLASS_SELECTOR"; +export type CSS_WORD = "CSS_WORD"; + +export type HTML_CUSTOM_ELEMENT = "HTML_CUSTOM_ELEMENT"; + +export type DOUBLE_QUOTED_STRING = "DOUBLE_QUOTED_STRING"; +export type SINGLE_QUOTED_STRING = "SINGLE_QUOTED_STRING"; + +export type NUMBER = "NUMBER"; +export type BOOL = "BOOL"; + +export type WHITESPACE = "WHITESPACE"; + +export type BREAK = "BREAK"; +export type NULL = "NULL"; + +export type LOGIC = "LOGIC"; +export type OPERATOR = "OPERATOR"; +export type BRACKET = "BRACKET"; + +export type BLOCKSTART = "BLOCKSTART"; +export type BLOCKEND = "BLOCKEND"; + +export type ANY_SYMBOL = "ANY_SYMBOL"; + +export type HASH_TAG = "HASH_TAG"; +export type URL = "URL"; + +export type CLASS = "CLASS"; + +export type ACCESS_MODIFIER = "ACCESS_MODIFIER"; + +export type SINGLE_LINE_COMMENT = "SINGLE_LINE_COMMENT"; +export type MULTI_LINE_COMMENT = "MULTI_LINE_COMMENT"; + +export type C_INSTRUCTION = "C_INSTRUCTION"; + +export type LexerType = + CWORD | + JSWORD | + PHPWORD | + + CFUNCTION | + CLASSNAME | + + CSS_ID_SELECTOR | + CSS_CLASS_SELECTOR | + CSS_WORD | + + HTML_CUSTOM_ELEMENT | + + DOUBLE_QUOTED_STRING | + SINGLE_QUOTED_STRING | + NUMBER | + BOOL | + WHITESPACE | + BREAK | + NULL | + + LOGIC | + OPERATOR | + BRACKET | + + BLOCKSTART | + BLOCKEND | + + ANY_SYMBOL | + + CLASS | + + ACCESS_MODIFIER | + SINGLE_LINE_COMMENT | + MULTI_LINE_COMMENT | + + C_INSTRUCTION +; + +export class LexerTypes +{ + static readonly CWORD:CWORD = "CWORD"; + static readonly JSWORD:JSWORD = "JSWORD"; + static readonly PHPWORD:PHPWORD = "PHPWORD"; + + static readonly CFUNCTION:CFUNCTION = "CFUNCTION"; + static readonly CLASSNAME:CLASSNAME = "CLASSNAME"; + + static readonly CSS_ID_SELECTOR:CSS_ID_SELECTOR = "CSS_ID_SELECTOR"; + static readonly CSS_CLASS_SELECTOR:CSS_CLASS_SELECTOR = "CSS_CLASS_SELECTOR"; + static readonly CSS_WORD:CSS_WORD = "CSS_WORD"; + + static readonly HTML_CUSTOM_ELEMENT:HTML_CUSTOM_ELEMENT = "HTML_CUSTOM_ELEMENT"; + + static readonly DOUBLE_QUOTED_STRING:DOUBLE_QUOTED_STRING = "DOUBLE_QUOTED_STRING"; + static readonly SINGLE_QUOTED_STRING:SINGLE_QUOTED_STRING = "SINGLE_QUOTED_STRING"; + static readonly NUMBER:NUMBER = "NUMBER"; + static readonly BOOL:BOOL = "BOOL"; + static readonly WHITESPACE:WHITESPACE = "WHITESPACE"; + static readonly BREAK:BREAK = "BREAK"; + static readonly NULL:NULL = "NULL"; + + static readonly LOGIC:LOGIC = "LOGIC"; + static readonly OPERATOR:OPERATOR = "OPERATOR"; + static readonly BRACKET:BRACKET = "BRACKET"; + + static readonly BLOCKSTART:BLOCKSTART = "BLOCKSTART"; + static readonly BLOCKEND:BLOCKEND = "BLOCKEND"; + + static readonly CLASS:CLASS = "CLASS"; + static readonly C_INSTRUCTION = "C_INSTRUCTION"; + + static readonly ANY_SYMBOL:ANY_SYMBOL = "ANY_SYMBOL"; + + static readonly HASH_TAG:HASH_TAG = "HASH_TAG"; + + static readonly URL:URL = "URL"; + + static readonly ACCESS_MODIFIER = "ACCESS_MODIFIER"; + + static readonly SINGLE_LINE_COMMENT = "SINGLE_LINE_COMMENT"; + static readonly MULTI_LINE_COMMENT = "MULTI_LINE_COMMENT"; + + static get IGNORE_TYPES():LexerType[] + { + let ignoreTypes:LexerType[] = [ + LexerTypes.SINGLE_LINE_COMMENT, + LexerTypes.DOUBLE_QUOTED_STRING, + LexerTypes.WHITESPACE, + LexerTypes.BREAK + ]; + + return ignoreTypes; + } + +} \ No newline at end of file diff --git a/browser/text/lexer/PHPClasses.ts b/browser/text/lexer/PHPClasses.ts new file mode 100644 index 0000000..1759885 --- /dev/null +++ b/browser/text/lexer/PHPClasses.ts @@ -0,0 +1,8 @@ +export class PHPClasses +{ + static list:string[] = + [ + "ReflectionClass", + "ReflectionNamedType" + ] +} \ No newline at end of file diff --git a/browser/text/lexer/PHPLexer.ts b/browser/text/lexer/PHPLexer.ts new file mode 100644 index 0000000..6cc0652 --- /dev/null +++ b/browser/text/lexer/PHPLexer.ts @@ -0,0 +1,77 @@ + +import { RegExpUtility } from "../RegExpUtitlity"; +import { Lexer } from "./Lexer"; +import { LexerMatcher } from "./LexerMatcher"; +import { LexerMatcherLibrary } from "./LexerMatcherLibrary"; +import { LexerType } from "./LexerType"; + +export type PHP_KEYWORD = "PHP_KEYWORD"; +export type PHP_START = "PHP_START"; +export type PHP_END = "PHP_END"; +export type PHP_CONSTANT = "PHP_CONSTANT"; + +export type PHPLexerType = LexerType | PHP_KEYWORD | PHP_START | PHP_END | PHP_CONSTANT; + +export class PHPLexer extends Lexer +{ + static readonly predefined = + [ + "NULL","E_CORE_ERROR","Exception","PATHINFO_FILENAME","JSON_PRETTY_PRINT","__DIR__", + "ENT_QUOTES","PREG_OFFSET_CAPTURE","PREG_SPLIT_DELIM_CAPTURE","PREG_SPLIT_NO_EMPTY", + "PHP_INT_MAX","SEEK_CUR","SEEK_SET", + + "CURLOPT_URL","CURLOPT_RETURNTRANSFER","CURLOPT_USERAGENT","E_USER_ERROR","E_USER_NOTICE","E_USER_WARNING", + + "JSON_ERROR_NONE", "JSON_ERROR_DEPTH", "JSON_ERROR_STATE_MISMATCH", "JSON_ERROR_CTRL_CHAR", + "JSON_ERROR_SYNTAX", "JSON_ERROR_UTF8", "JSON_ERROR_RECURSION", "JSON_ERROR_INF_OR_NAN", + "JSON_ERROR_UNSUPPORTED_TYPE", "JSON_ERROR_INVALID_PROPERTY_NAME", "JSON_ERROR_UTF16" + ] + + static readonly PHP_CONSTANT:PHP_CONSTANT = "PHP_CONSTANT"; + static readonly PHP_CONSTANT_MATCHER = + new LexerMatcher( PHPLexer.PHP_CONSTANT, RegExpUtility.createFromList( PHPLexer.predefined ) ); + + static readonly PHP_KEYWORD:PHP_KEYWORD = "PHP_KEYWORD"; + + static readonly PHP_KEYWORD_MATCHER = + new LexerMatcher( PHPLexer.PHP_KEYWORD, /include_once|include|require_once|require|extends|function|const/ ); + + static readonly PHP_START:PHP_START = "PHP_START"; + + static readonly PHP_START_MATCHER = + new LexerMatcher( PHPLexer.PHP_START, /\<\?php/ ); + + static readonly PHP_END:PHP_END = "PHP_END"; + + static readonly PHP_END_MATCHER = + new LexerMatcher( PHPLexer.PHP_END, /\?\>/ ); + + constructor() + { + super(); + + this.addAllMatchers( + LexerMatcherLibrary.SINGLE_LINE_COMMENT_MATCHER, + LexerMatcherLibrary.MULTI_LINE_COMMENT_MATCHER, + LexerMatcherLibrary.DOUBLE_QUOTED_STRING_MATCHER, + LexerMatcherLibrary.SINGLE_QUOTED_STRING_MATCHER, + PHPLexer.PHP_CONSTANT_MATCHER, + PHPLexer.PHP_START_MATCHER, + PHPLexer.PHP_END_MATCHER, + LexerMatcherLibrary.NUMBER_MATCHER, + LexerMatcherLibrary.NULL_MATCHER, + LexerMatcherLibrary.BOOL_MATCHER, + LexerMatcherLibrary.BREAK_MATCHER, + LexerMatcherLibrary.WHITESPACE_MATCHER, + LexerMatcherLibrary.LOGIC_MATCHER, + LexerMatcherLibrary.BRACKET_MATCHER, + LexerMatcherLibrary.ACCESS_MODIFIER_MATCHER, + LexerMatcherLibrary.CLASS_MATCHER, + PHPLexer.PHP_KEYWORD_MATCHER, + LexerMatcherLibrary.OPERATOR_MATCHER, + LexerMatcherLibrary.PHPWORD_MATCHER, + LexerMatcherLibrary.ANY_SYMBOL_MATCHER + ); + + } +} \ No newline at end of file diff --git a/browser/text/lexer/expressions/BinaryExpression.ts b/browser/text/lexer/expressions/BinaryExpression.ts new file mode 100644 index 0000000..388b1d6 --- /dev/null +++ b/browser/text/lexer/expressions/BinaryExpression.ts @@ -0,0 +1,13 @@ +import { SourceInfo } from "../parsing/SourceInfo"; +import { ExpressionNode } from "./ExpressionNode"; + +export class BinaryExpression extends ExpressionNode +{ + constructor( left:ExpressionNode, right:ExpressionNode, data:T, sourceInfo:SourceInfo ) + { + super( [ left, right ], data, sourceInfo ); + } + + get left(){ return this._children[ 0 ]; } + get right(){ return this._children[ 1 ]; } +} \ No newline at end of file diff --git a/browser/text/lexer/expressions/ExpressionNode.ts b/browser/text/lexer/expressions/ExpressionNode.ts new file mode 100644 index 0000000..f904d33 --- /dev/null +++ b/browser/text/lexer/expressions/ExpressionNode.ts @@ -0,0 +1,15 @@ +import { SourceInfo } from "../parsing/SourceInfo"; + +export class ExpressionNode +{ + _sourceInfo:SourceInfo; + _children:ExpressionNode[]; + _data:T; + + constructor( children:ExpressionNode[], data:T, sourceInfo:SourceInfo ) + { + this._children = children; + this._data = data; + this._sourceInfo = sourceInfo; + } +} \ No newline at end of file diff --git a/browser/text/lexer/expressions/LiteralNode.ts b/browser/text/lexer/expressions/LiteralNode.ts new file mode 100644 index 0000000..fd21906 --- /dev/null +++ b/browser/text/lexer/expressions/LiteralNode.ts @@ -0,0 +1,12 @@ +import { SourceInfo } from "../parsing/SourceInfo"; +import { ExpressionNode } from "./ExpressionNode"; + +export class LiteralNode extends ExpressionNode +{ + constructor( literal:ExpressionNode, data:T, sourceInfo:SourceInfo ) + { + super( [ literal ], data, sourceInfo ); + } + + get value(){ return this._children[ 0 ] } +} diff --git a/browser/text/lexer/library/CSharpLanguage.ts b/browser/text/lexer/library/CSharpLanguage.ts new file mode 100644 index 0000000..fb7f4b4 --- /dev/null +++ b/browser/text/lexer/library/CSharpLanguage.ts @@ -0,0 +1,13 @@ +export class CSharpLanguage +{ + public static readonly token ="c#"; + + public static readonly keywords:string[] = + [ + "var","new","void","short","long","uint", + "float","int","byte","bool","double","string", + "using" + ] + + +} \ No newline at end of file diff --git a/browser/text/lexer/library/GodotShader.ts b/browser/text/lexer/library/GodotShader.ts new file mode 100644 index 0000000..970e6ea --- /dev/null +++ b/browser/text/lexer/library/GodotShader.ts @@ -0,0 +1,21 @@ +export class GodotShader +{ + public static readonly token ="gdshader"; + + public static readonly keywords:string[] = + [ + "shader_type","uniform","void", + "sampler2D","vec4","vec3","vec2","float","void" + ] + + public static readonly keywords2:string[] = + [ + "canvas_item","spatial", + "hint_screen_texture", + "repeat_disable", + "filter_linear_mipmap", + "COLOR","UV","ALBEDO" + ] + + +} \ No newline at end of file diff --git a/browser/text/lexer/library/SteamworksNetLibrary.ts b/browser/text/lexer/library/SteamworksNetLibrary.ts new file mode 100644 index 0000000..f00a8f5 --- /dev/null +++ b/browser/text/lexer/library/SteamworksNetLibrary.ts @@ -0,0 +1,15 @@ +import { UnityLibrary } from "./UnityLibrary"; + +export class SteamworksNetLibrary +{ + public static readonly token = "steamworks-net"; + public static keywords:string[] = + [ + "SteamAPI","SteamFriends","SteamUser","SteamUserStats", + "CallResult", + + "SteamLeaderboard_t","SteamLeaderboardEntries_t","LeaderboardScoresDownloaded_t","ELeaderboardDataRequest", + "ELeaderboardUploadScoreMethod" + ] + +} \ No newline at end of file diff --git a/browser/text/lexer/library/UnityLibrary.ts b/browser/text/lexer/library/UnityLibrary.ts new file mode 100644 index 0000000..42ff40a --- /dev/null +++ b/browser/text/lexer/library/UnityLibrary.ts @@ -0,0 +1,17 @@ +import { CSharpLanguage as CSharpLanguage } from "./CSharpLanguage"; +import { SteamworksNetLibrary } from "./SteamworksNetLibrary"; + +export class UnityLibrary +{ + public static readonly token = "unity"; + public static keywords:string[] = + CSharpLanguage.keywords.concat( + [ + "Vector2","Vector3","Vector4", + "Transform","MonoBehaviour","GameObject", + "Undo", + "PrefabUtility", + ].concat( SteamworksNetLibrary.keywords ) + ) + +} \ No newline at end of file diff --git a/browser/text/lexer/parsing/OperatorResolver.ts b/browser/text/lexer/parsing/OperatorResolver.ts new file mode 100644 index 0000000..5775c19 --- /dev/null +++ b/browser/text/lexer/parsing/OperatorResolver.ts @@ -0,0 +1,4 @@ +export class OperatorResolver +{ + +} \ No newline at end of file diff --git a/browser/text/lexer/parsing/Parser.ts b/browser/text/lexer/parsing/Parser.ts new file mode 100644 index 0000000..f793f8f --- /dev/null +++ b/browser/text/lexer/parsing/Parser.ts @@ -0,0 +1,73 @@ +import { MultiString } from "../../../i18n/MultiString"; +import { MessageType, MessageTypes } from "../../../messages/MessageType"; +import { ParserMessage } from "./ParserMessage"; +import { ParserPhase } from "./ParserPhase"; +import { SourceInfo } from "./SourceInfo"; + +export abstract class Parser +{ + _phases:ParserPhase[] = []; + _source:string; + get source(){ return this._source; } + _messages:ParserMessage[] = []; + get messages():ParserMessage[]{ return this._messages } + + _add( phase:ParserPhase ) + { + this._phases.push( phase ); + } + + get hasErrors() + { + return this._messages.find( m => MessageTypes.Error == m.type ); + } + + _message( type:MessageType, content:string|MultiString, sourceInfo:SourceInfo ) + { + this._messages.push( ParserMessage.create( type, MultiString.resolve( content ), sourceInfo ) ); + } + + verbose( content:string|MultiString, sourceInfo?:SourceInfo ) + { + this._message( MessageTypes.Verbose, content, sourceInfo ); + } + + info( content:string|MultiString, sourceInfo?:SourceInfo ) + { + this._message( MessageTypes.Info, content, sourceInfo ); + } + + warn( content:string|MultiString, sourceInfo?:SourceInfo ) + { + this._message( MessageTypes.Warning, content, sourceInfo ); + } + + error( content:string|MultiString, sourceInfo?:SourceInfo ) + { + this._message( MessageTypes.Error, content, sourceInfo ); + } + + + parse( source:string ) + { + this._source = source; + + for ( let phase of this._phases ) + { + phase.reset(); + } + + for ( let phase of this._phases ) + { + phase.process(); + + if ( this.hasErrors ) + { + this.error( "Stopped at: " + phase.phaseInfo ); + return; + } + } + + this.info( "Parsed successfully" ); + } +} \ No newline at end of file diff --git a/browser/text/lexer/parsing/ParserMessage.ts b/browser/text/lexer/parsing/ParserMessage.ts new file mode 100644 index 0000000..f3ec62a --- /dev/null +++ b/browser/text/lexer/parsing/ParserMessage.ts @@ -0,0 +1,20 @@ +import { Message } from "../../../messages/Message"; +import { MessageType } from "../../../messages/MessageType"; +import { SourceInfo } from "./SourceInfo"; + + +export class ParserMessage extends Message +{ + sourceInfo:SourceInfo; + + static create( type:MessageType, content:string, sourceInfo:SourceInfo ) + { + let m = new ParserMessage(); + + m.type = type; + m.content = content; + m.sourceInfo = sourceInfo; + + return m; + } +} \ No newline at end of file diff --git a/browser/text/lexer/parsing/ParserPhase.ts b/browser/text/lexer/parsing/ParserPhase.ts new file mode 100644 index 0000000..edd3cdf --- /dev/null +++ b/browser/text/lexer/parsing/ParserPhase.ts @@ -0,0 +1,21 @@ +import { Parser } from "./Parser"; + +export abstract class ParserPhase

+{ + #parser:P; + + get parser(){ return this.#parser; } + + abstract get phaseInfo():string; + + constructor( parser:P ) + { + this.#parser = parser; + this.#parser._add( this ); + } + + reset(){} + + abstract process():void; + +} \ No newline at end of file diff --git a/browser/text/lexer/parsing/PrecedenceLevel.ts b/browser/text/lexer/parsing/PrecedenceLevel.ts new file mode 100644 index 0000000..957043d --- /dev/null +++ b/browser/text/lexer/parsing/PrecedenceLevel.ts @@ -0,0 +1,7 @@ +import { OperatorResolver } from "./OperatorResolver"; + +export class PrecedenceLevel +{ + fromLeft:boolean = true; + operatorResolver:OperatorResolver[]; +} \ No newline at end of file diff --git a/browser/text/lexer/parsing/SourceInfo.ts b/browser/text/lexer/parsing/SourceInfo.ts new file mode 100644 index 0000000..0d63916 --- /dev/null +++ b/browser/text/lexer/parsing/SourceInfo.ts @@ -0,0 +1,87 @@ +import { RJExpressionNode } from "../../../rj/expressions/RJExpressionNode"; +import { LexerEvent } from "../LexerEvent"; + +export class SourceRange +{ + offset:number; + length:number; + + get exclusiveEnd() + { + return this.offset + this.length; + } + + get lastSourceIndex() + { + return this.exclusiveEnd - 1; + } + + + + static create( offset:number, length:number ) + { + let range = new SourceRange(); + range.offset = offset; + range.length = length; + + return range; + } + + static combine( a:SourceRange, b:SourceRange ) + { + let start = Math.min( a.offset, b.offset ); + let end = Math.max( a.exclusiveEnd, b.exclusiveEnd ); + + let range = new SourceRange(); + range.offset = start; + range.length = end - start; + + return range; + } + + static combineNodes( a:RJExpressionNode, b:RJExpressionNode ) + { + return SourceRange.combine( a.sourceRange, b.sourceRange ); + } + + static fromNodes( nodes:RJExpressionNode[], start:number, end:number ) + { + return SourceRange.combine( nodes[ start ].sourceRange, nodes[ end ].sourceRange ); + } + + static fromAllNodes( nodes:RJExpressionNode[] ) + { + return SourceRange.fromNodes( nodes, 0, nodes.length - 1 ); + } + + + +} + +export class SourceInfo +{ + range:SourceRange; + + static asRange( start:number, length:number ) + { + let sourceInfo = new SourceInfo(); + + sourceInfo.range = SourceRange.create( start, length ); + + return sourceInfo; + } + + static fromRange( range:SourceRange ) + { + let sourceInfo = new SourceInfo(); + + sourceInfo.range = range; + + return sourceInfo; + } + + static fromEvent( le:LexerEvent ) + { + return SourceInfo.asRange( le.offset, le.length ); + } +} \ No newline at end of file diff --git a/browser/text/replacing/TextReplacement.ts b/browser/text/replacing/TextReplacement.ts new file mode 100644 index 0000000..c8700c5 --- /dev/null +++ b/browser/text/replacing/TextReplacement.ts @@ -0,0 +1,21 @@ +export abstract class TextReplacement +{ + abstract replace( source:string ):string; +} + +export class RegexReplacement extends TextReplacement +{ + regex:RegExp; + replacement:string; + + toString() + { + let info = `RegexReplacement{${this.regex} >> '${this.replacement}'`; + return info; + } + + replace( source:string ) + { + return source.replace( this.regex, this.replacement ); + } +} \ No newline at end of file diff --git a/browser/text/replacing/TextReplacementProcessor.ts b/browser/text/replacing/TextReplacementProcessor.ts new file mode 100644 index 0000000..0544618 --- /dev/null +++ b/browser/text/replacing/TextReplacementProcessor.ts @@ -0,0 +1,24 @@ +import { Files } from "../../node/Files"; +import { TextReplacer } from "./TextReplacer"; + +export class TextReplacementProcessor +{ + replacers:TextReplacer[] = []; + + process( source:string ):string + { + let text = source; + + this.replacers.forEach( r => text = r.process( text ) ); + + return text; + } + + replaceFile( filePath:string, suffix:string = ".replaced.html") + { + let newFilePath = filePath + suffix; + let text = Files.loadUTF8( filePath ); + let replacedText = this.process( text ); + Files.saveUTF8( newFilePath, replacedText ); + } +} \ No newline at end of file diff --git a/browser/text/replacing/TextReplacer.ts b/browser/text/replacing/TextReplacer.ts new file mode 100644 index 0000000..e1add5a --- /dev/null +++ b/browser/text/replacing/TextReplacer.ts @@ -0,0 +1,134 @@ +import { ExtendedRegex } from "../ExtendedRegex"; +import { RegExpUtility } from "../RegExpUtility"; +import { RegexReplacement, TextReplacement } from "./TextReplacement"; +import { TextSelectionRange } from "./TextSelectionRange"; +import { RegexStartEndSelector, TextSelector } from "./TextSelector"; + +export class TextReplacer +{ + selector:TextSelector; + replacements:TextReplacement[] = []; + + process( source:string ):string + { + let ranges = this.selector ? + this.selector.select( source ) : + [ TextSelectionRange.fromString( source ) ]; + + let replacedRanges:string[] = []; + + if ( this.selector ) + { + console.log( + "RANGES", this.selector + "", + ranges.map( + r => + r.start === 0 && r.length === source.length ? ( "complete source (" + source.length+ ")" ) + : ( r.start + ":" + r.end ) + ).join() + ); + } + + + ranges.forEach( + range => + { + let replacingSelection = range.get( source ); + this.replacements.forEach( r => { replacingSelection = r.replace( replacingSelection ); } ); + replacedRanges.push( replacingSelection ); + } + ); + + let replacedSource = TextSelectionRange.replaceRanges( source, ranges, replacedRanges ); + + return replacedSource; + } + + setRegexStartEndSelection( start:RegExp, end:RegExp, inner:boolean = false ) + { + let regexSelector = new RegexStartEndSelector(); + regexSelector.startRegex = start; + regexSelector.endRegex = end; + regexSelector.inner = inner; + regexSelector.multiple = true; + + this.selector = regexSelector; + } + + setOuterTagSelection( tag:string ) + { + this.setRegexStartEndSelection( new RegExp( `<${tag}` ), new RegExp( `<\\/${tag}>` ) ) + } + + setInnerTagSelection( tag:string ) + { + this.setRegexStartEndSelection( new RegExp( `<${tag}` ), new RegExp( `<\\/${tag}>` ), true ) + } + + addRegexReplacement( match:RegExp, replacement:string ) + { + let regexReplacement = new RegexReplacement(); + regexReplacement.regex = match; + regexReplacement.replacement = replacement; + + this.replacements.push( regexReplacement ); + } + + static readonly attributeInnerRegExp = /()(\z)(<\/TAG>)/g; + + addTagAttributeToInnerTagReplacement( tag:string, attribute:string, innerTag:string ) + { + let regexSource = TextReplacer.attributeInnerRegExp.source.replace( /TAG/g, tag ); + regexSource = regexSource.replace( /ATTRIBUTE/g, attribute ); + + let matcher = ExtendedRegex.create( regexSource ); + console.log( "ATTRIBUTE TO INNER", matcher ); + let replacement = `$1$3<${innerTag}>$2$4`; + + this.addRegexReplacement( matcher, replacement ); + } + + addTagRenaming( oldTag:string, newTag:string ) + { + let matcher = RegExpUtility.createRegExp( /(<\/?)TAG/g, "TAG", oldTag ); + let replacement = `$1${newTag}`; + + this.addRegexReplacement( matcher, replacement ); + } + + static readonly removeAttributeREgex = /(<\P\z)ATTRIBUTE\s*=\s*".*"(\z>\z<\/\P>)/g; + + addAttributeRemoving( attribute:string) + { + let regexSource = TextReplacer.removeAttributeREgex.source.replace( /ATTRIBUTE/g, attribute ); + + let matcher = ExtendedRegex.create( regexSource ); + + let replacement = `$1 $2`; + + this.addRegexReplacement( matcher, replacement ); + } + + static create( match:RegExp, replacement:string ) + { + let replacer = new TextReplacer(); + replacer.addRegexReplacement( match, replacement ); + return replacer; + } + + static createInRegexSelection( match:RegExp, replacement:string, selectionStart:RegExp, selectionEnd:RegExp ) + { + let replacer = new TextReplacer(); + replacer.setRegexStartEndSelection( selectionStart, selectionEnd ); + replacer.addRegexReplacement( match, replacement ); + return replacer; + } + + static createInTag( match:RegExp, replacement:string, tag:string ) + { + let replacer = new TextReplacer(); + replacer.setRegexStartEndSelection( new RegExp( `<${tag}` ), new RegExp( `<\\/${tag}>` ) ); + replacer.addRegexReplacement( match, replacement ); + return replacer; + } +} \ No newline at end of file diff --git a/browser/text/replacing/TextSelectionRange.ts b/browser/text/replacing/TextSelectionRange.ts new file mode 100644 index 0000000..0fb0487 --- /dev/null +++ b/browser/text/replacing/TextSelectionRange.ts @@ -0,0 +1,99 @@ +export class TextSelectionRange +{ + start:number; + length:number; + + get end():number + { + return this.start + this.length; + } + + set end( value:number ) + { + this.length = value - this.start; + } + + get( source:string ) + { + return source.substring( this.start, this.end ); + } + + replace( source:string, replacement:string ) + { + let before = source.substring( 0, this.start ); + let after = source.substring( this.end ); + + console.log( "start:", this.start, "end:", this.end) + + return [ before, replacement, after ].join( "" ); + } + + static fromWithLength( start:number, length:number ) + { + let range = new TextSelectionRange(); + range.start = start; + range.length = length; + + return range; + } + + static fromToExclusive( start:number, end:number ) + { + let range = new TextSelectionRange(); + + range.start = start; + range.end = end; + + return range; + } + + static fromString( source:string ) + { + let range = new TextSelectionRange(); + range.start = 0; + range.length = source.length; + + return range; + } + + static replaceRanges( source:string, ranges:TextSelectionRange[], replacements:string[] ) + { + let fragments:string[] = []; + let lastEnd = 0; + + + + for ( let i = 0; i < ranges.length; i++ ) + { + let range = ranges[ i ]; + let replacement = replacements[ i ]; + + let beforeRange = source.substring( lastEnd, range.start ); + + fragments.push( beforeRange ); + fragments.push( replacement ); + + lastEnd = range.end; + + if ( i !== ( ranges.length - 1 ) ) + { + continue; + } + + let afterRange = source.substring( lastEnd, source.length ); + + fragments.push( afterRange ); + } + + let replaced = fragments.join( "" ); + + if ( replaced === undefined ) + { + console.log( "Merging ranges failed:", ranges.length, fragments.length ); + } + + return replaced; + + + } +} diff --git a/browser/text/replacing/TextSelector.ts b/browser/text/replacing/TextSelector.ts new file mode 100644 index 0000000..998d930 --- /dev/null +++ b/browser/text/replacing/TextSelector.ts @@ -0,0 +1,128 @@ +import { RegExpUtility } from "../RegExpUtility"; +import { TextSelectionRange } from "./TextSelectionRange"; +import { ColorConsole } from "../../node/ColorConsole"; + +export abstract class TextSelector +{ + abstract select( source:string ):TextSelectionRange[] +} + +export class RegexStartEndSelector extends TextSelector +{ + startRegex:RegExp; + endRegex:RegExp; + inner:boolean = false; + multiple:boolean = true; + maxRanges:number = 100; + + toString() + { + let start = ColorConsole.fg( this.startRegex.source, ColorConsole.red ); + let end = ColorConsole.fg( this.endRegex.source, ColorConsole.red ); + let info = `RegexSelector{${start} ${end} ${this.multiple?"multiple":""} ${this.inner?"inner":""}}`; + return info; + } + + select( source:string ) + { + this.startRegex = RegExpUtility.makeGlobal( this.startRegex ); + this.endRegex = RegExpUtility.makeGlobal( this.endRegex ); + + if ( this.multiple ) + { + return this.selectMultiple( source ); + } + + let range = TextSelectionRange.fromString( source ); + + this.startRegex.lastIndex = 0; + this.endRegex.lastIndex = 0; + + let startResult = this.startRegex.exec( source ); + + if ( startResult ) + { + range.start = this.inner ? ( startResult.index + startResult[ 0 ].length ) : startResult.index; + } + else + { + console.log( "Start not found: ", this.startRegex, + source.substring( 0, Math.min( source.length,100 ) ) ); + } + + let endResult = this.endRegex.exec( source ); + + if ( endResult ) + { + range.end = this.inner ? endResult.index : ( endResult.index + endResult[ 0 ].length ); + } + else + { + console.log( "End not found: ", this.endRegex, + source.substring( 0, Math.min( source.length,100 ) ) ); + } + + return [ range ]; + } + + selectMultiple( source:string ):TextSelectionRange[] + { + let ranges:TextSelectionRange[] = []; + + let currentIndex = -1; + let searching = true; + + let currentElements = 0; + + while ( searching && currentElements < this.maxRanges ) + { + let starterIndex = Math.max( 0, currentIndex ); + this.startRegex.lastIndex = starterIndex; + let startResult = this.startRegex.exec( source ); + + if ( ! startResult || startResult.index <= currentIndex ) + { + if ( startResult ) + { + console.log( "Start Index invalid:", currentIndex, startResult.index ); + } + + searching = false; + break; + } + + this.endRegex.lastIndex = startResult.index + startResult[ 0 ].length; + + let endResult = this.endRegex.exec( source ); + + if ( ! endResult || endResult.index <= currentIndex ) + { + if ( endResult ) + { + console.log( "End Index invalid:", currentIndex, endResult.index ); + } + + searching = false; + break; + } + + let range = new TextSelectionRange(); + range.start = this.inner ? ( startResult.index + startResult[ 0 ].length ) : startResult.index; + range.end = this.inner ? endResult.index : ( endResult.index + endResult[ 0 ].length ); + + ranges.push( range ); + + currentIndex = endResult.index + endResult[ 0 ].length; + searching = currentIndex < source.length; + currentElements ++; + } + + if ( ranges.length === 0 ) + { + console.log( "No ranges found!" ); + ranges.push( TextSelectionRange.fromString( source ) ); + } + + return ranges; + } +} \ No newline at end of file diff --git a/browser/text/replacing/VariableReplacer.ts b/browser/text/replacing/VariableReplacer.ts new file mode 100644 index 0000000..7746c87 --- /dev/null +++ b/browser/text/replacing/VariableReplacer.ts @@ -0,0 +1,16 @@ +import { RegExpUtility } from "../RegExpUtility"; + +export type Variables = {[index:string]:string } +export class VariableReplacer +{ + static replace( source:string, variables:Variables ) + { + for ( let it in variables ) + { + let regexSource = RegExpUtility.toRegexSource( "${" + it + "}" ); + source = source.replace( new RegExp( regexSource, "g" ), variables[ it ] ); + } + + return source; + } +} \ No newline at end of file diff --git a/browser/tools/Arrays.ts b/browser/tools/Arrays.ts new file mode 100644 index 0000000..d88ef9b --- /dev/null +++ b/browser/tools/Arrays.ts @@ -0,0 +1,179 @@ +export class Arrays +{ + + static combine( t:T, array:T[] ):T[] + { + let combined = [ t ]; + + if ( array ) + { + combined = combined.concat( array ); + } + + return combined; + } + + static combine2( t:T, t2:T, array:T[] ):T[] + { + let combined = [ t, t2 ]; + + if ( array ) + { + combined = combined.concat( array ); + } + + return combined; + } + + static combine3( t:T, t2:T, t3:T, array:T[] ):T[] + { + let combined = [ t, t2, t3 ]; + + if ( array ) + { + combined = combined.concat( array ); + } + + return combined; + } + + static pushIfNotInside( array:T[], element:T ) + { + let index = array.indexOf( element ); + + if ( index !== - 1) + { + return; + } + + array.push( element ); + } + + + static create( size:number, creator:( i:number ) => T ):T[] + { + let created:T[] = []; + + for ( let i = 0; i < size; i++ ) + { + let t = creator( i ); + created.push( t ); + } + + return created; + } + + static isEmpty( source:T[] ) + { + return source.length === 0; + } + + static isLast( source:T[], index:number ) + { + return index === ( source.length -1 ) ; + } + + static getLast( source:T[] ):T + { + return source[ source.length - 1 ]; + } + + static forEachIndex( source:T[], callback:( t:T, index:number, normalized?:number )=>void ) + { + let normalizer = 1 / ( source.length - 1 ); + + for ( let i = 0; i < source.length; i++ ) + { + let normalized = i * normalizer; + callback( source[ i ], i, normalized ); + } + } + + static transfer( source:T[], target:T[] ) + { + this.transferRange( source, 0, target, 0, source.length ); + } + + static transferRange( source:T[], sourceOffset:number, target:T[], targetOffset:number, numItems:number ) + { + for ( let i = 0; i < numItems; i++ ) + { + target[ i + targetOffset ] = source[ i + sourceOffset ]; + } + } + + static copyRange( array:T[], start:number, length:number ) + { + return array.slice( start, start + length ); + } + + static copyRangeFromStart( array:T[], start:number ) + { + return array.slice( start, array.length ); + } + + static insert( array:T[], element:T, index:number ) + { + array.splice( index, 0, element ); + } + + static moveToFront( array:T[], element:T ) + { + if ( array[ 0 ] === element ) + { + return; + } + + Arrays.remove( array, element ); + Arrays.insert( array, element, 0 ); + } + + static remove( array:T[], element:T ) + { + if ( ! array || ! element ) + { + return; + } + + let index = array.indexOf( element ); + + if ( index === -1 ) + { + return; + } + + array.splice( index, 1 ); + } + + static addIfNotPresent( array:T[], t:T) + { + let index = array.indexOf( t ); + + if ( index !== -1 ) + { + return; + } + + array.push( t ); + } + + static removeAt( array:T[], index:number ) + { + if ( ! array ) + { + return; + } + + if ( index < 0 || index > array.length ) + { + return; + } + + array.splice( index, 1 ); + } + + static removeAllAfter( array:any[], index:number ) + { + array.splice( index, array.length - index ); + } +} \ No newline at end of file diff --git a/browser/tsconfig.json b/browser/tsconfig.json new file mode 100644 index 0000000..8155182 --- /dev/null +++ b/browser/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": + { + + "outDir": "../../builds/rokojori-com/", + "sourceMap": true, + "noImplicitAny": true, + "module": "commonjs", + "target": "es2020", + "baseUrl": ".", + "paths": + { + }, + + "typeRoots": [ "node_modules/@types","node_modules/@types/three/"], + + } + + +} \ No newline at end of file diff --git a/node/tsconfig.json b/node/tsconfig.json new file mode 100644 index 0000000..f2e3a69 --- /dev/null +++ b/node/tsconfig.json @@ -0,0 +1,7 @@ +{ + "compilerOptions": { + "lib": ["ES2020"], + "types": ["node"], // Includes Node.js types + }, + "include": ["./*"] +} \ No newline at end of file