Initial Commit

This commit is contained in:
Josef 2025-03-08 09:16:54 +01:00
commit 3671fc067f
74 changed files with 8346 additions and 0 deletions

182
browser/colors/Colors.ts Normal file
View File

@ -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" );
}

410
browser/colors/HSLColor.ts Normal file
View File

@ -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 ] );
}
}

145
browser/colors/HTMLColor.ts Normal file
View File

@ -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 );
}
}

View File

@ -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"
}
}

View File

@ -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 );
}
}

221
browser/colors/RGBColor.ts Normal file
View File

@ -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 ] );
}
}

View File

@ -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 );
}
}

232
browser/dom/ClassFlag.ts Normal file
View File

@ -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 );
}
}

162
browser/dom/DOMEditor.ts Normal file
View File

@ -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}</${tag}>`;
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 );
}
}

View File

@ -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';
}

View File

@ -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<T=Element>( element:Element|Document )
{
return element.querySelector( this.selector ) as any as T;
}
isQueriedChecked( element:Element )
{
let queried = this.query<HTMLInputElement>( 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 ) );
}
}

172
browser/dom/ElementType.ts Normal file
View File

@ -0,0 +1,172 @@
import { DOMEditor } from "./DOMEditor";
export class ElementType<E extends Element>
{
static readonly any = new ElementType<Element>( "*" );
static readonly input = new ElementType<HTMLInputElement>( "input" );
static readonly meta = new ElementType<HTMLMetaElement>( "meta" );
static readonly title = new ElementType<HTMLMetaElement>( "title" );
static readonly anchor = new ElementType<HTMLAnchorElement>( "a" );
static readonly break = new ElementType<HTMLBRElement>( "br" );
static readonly image = new ElementType<HTMLImageElement>( "img" );
static readonly audio = new ElementType<HTMLAudioElement>( "audio" );
static readonly video = new ElementType<HTMLVideoElement>( "video" );
static readonly source = new ElementType<HTMLSourceElement>( "source" );
static readonly canvas = new ElementType<HTMLCanvasElement>( "canvas" );
static readonly script = new ElementType<HTMLScriptElement>( "script" );
static readonly style = new ElementType<HTMLStyleElement>( "style" );
static readonly form = new ElementType<HTMLFormElement>( "form" );
static readonly button = new ElementType<HTMLButtonElement>( "button" );
static readonly select = new ElementType<HTMLSelectElement>( "select" );
static readonly option = new ElementType<HTMLOptionElement>( "option" );
static readonly textArea = new ElementType<HTMLTextAreaElement>( "textarea" );
static readonly table = new ElementType<HTMLTableElement>( "table" );
static readonly thead = new ElementType<HTMLTableElement>( "thead" );
static readonly tbody = new ElementType<HTMLTableElement>( "tbody" );
static readonly tr = new ElementType<HTMLTableRowElement>( "tr" );
static readonly th = new ElementType<HTMLTableCellElement>( "th" );
static readonly td = new ElementType<HTMLTableCellElement>( "td" );
static readonly hr = new ElementType<HTMLElement>( "hr" );
static readonly span = new ElementType<HTMLSpanElement>( "span" );
static readonly div = new ElementType<HTMLDivElement>( "div" );
static readonly code = new ElementType<HTMLElement>( "code" );
static readonly pre = new ElementType<HTMLPreElement>( "pre" );
static readonly p = new ElementType<HTMLElement>( "p" );
static readonly b = new ElementType<HTMLElement>( "b" );
static readonly i = new ElementType<HTMLElement>( "i" );
static readonly h1 = new ElementType<HTMLElement>( "h1" );
static readonly h2 = new ElementType<HTMLElement>( "h2" );
static readonly h3 = new ElementType<HTMLElement>( "h3" );
static readonly h4 = new ElementType<HTMLElement>( "h4" );
static readonly h5 = new ElementType<HTMLElement>( "h5" );
static readonly h6 = new ElementType<HTMLElement>( "h6" );
static readonly h7 = new ElementType<HTMLElement>( "h7" );
static readonly h8 = new ElementType<HTMLElement>( "h8" );
static readonly h9 = new ElementType<HTMLElement>( "h9" );
static readonly h10 = new ElementType<HTMLElement>( "h10" );
static readonly section = new ElementType<HTMLElement>( "section" );
static readonly iframe = new ElementType<HTMLIFrameElement>( "iframe" );
static readonly svg = new ElementType<SVGElement>( "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;
}
}

View File

@ -0,0 +1,101 @@
export abstract class AsyncBooleanExpression<T>
{
abstract evaluate( t:T ):Promise<boolean>;
NOT():AsyncBooleanExpression<T>
{
return new NotAsyncExpression<T>( this );
}
AND( matcher:AsyncBooleanExpression<T>):AsyncBooleanExpression<T>
{
return new AndAsyncExpression<T>( [ this, matcher ] );
}
OR( matcher:AsyncBooleanExpression<T>):AsyncBooleanExpression<T>
{
return new OrAsyncExpression<T>( [ this, matcher ] );
}
}
export class NotAsyncExpression<T> extends AsyncBooleanExpression<T>
{
private _matcher:AsyncBooleanExpression<T>;
constructor( matcher:AsyncBooleanExpression<T> )
{
super();
this._matcher = matcher;
}
async evaluate( t:T ):Promise<boolean>
{
let result = await this._matcher.evaluate( t );
return Promise.resolve( ! result );
}
NOT()
{
return this._matcher;
}
}
export abstract class ListInputAsyncExpression<T> extends AsyncBooleanExpression<T>
{
protected _matchers:AsyncBooleanExpression<T>[];
constructor( matchers:AsyncBooleanExpression<T>[] )
{
super();
this._matchers = matchers;
}
}
export class AndAsyncExpression<T> extends ListInputAsyncExpression<T>
{
async evaluate( t:T ):Promise<boolean>
{
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<T> )
{
this._matchers.push( matcher );
return this;
}
}
export class OrAsyncExpression<T> extends ListInputAsyncExpression<T>
{
async evaluate( t:T ):Promise<boolean>
{
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<T> )
{
this._matchers.push( matcher );
return this;
}
}

View File

@ -0,0 +1,76 @@
import { AsyncBooleanExpression } from "./AsyncBooleanExpression";
export abstract class StringValueAsyncMatcher<T> extends AsyncBooleanExpression<T>
{
abstract getStringValue( t:T ):Promise<string>;
}
export abstract class LitaralAsyncMatcher<T,L extends string> extends StringValueAsyncMatcher<T>
{
_literal:L
constructor( literal:L )
{
super();
this._literal = literal;
}
async evaluate( t:T ):Promise<boolean>
{
let value = await this.getStringValue( t );
if ( value === null )
{
return false;
}
return Promise.resolve( this._literal === value );
}
}
export abstract class ContainsLitaralAsyncMatcher<T,L extends string> extends StringValueAsyncMatcher<T>
{
_literal:L
constructor( literal:L )
{
super();
this._literal = literal;
}
async evaluate( t:T ):Promise<boolean>
{
let value = await this.getStringValue( t );
if ( value === null )
{
return false;
}
return Promise.resolve( value.indexOf( this._literal ) !== -1 );
}
}
export abstract class RegExpAsyncMatcher<T> extends StringValueAsyncMatcher<T>
{
private _regexp:RegExp;
constructor( regexp:RegExp )
{
super();
this._regexp = regexp;
}
async evaluate( t:T ):Promise<boolean>
{
let value = await this.getStringValue( t );
if ( value === null )
{
return false;
}
return Promise.resolve( this._regexp.test( value ) );
}
}

View File

@ -0,0 +1,114 @@
export abstract class BooleanExpression<T>
{
abstract evaluate( t:T ):boolean;
NOT():BooleanExpression<T>
{
return new NotExpression<T>( this );
}
AND( matcher:BooleanExpression<T>):BooleanExpression<T>
{
return new AndExpression<T>( [ this, matcher ] );
}
OR( matcher:BooleanExpression<T>):BooleanExpression<T>
{
return new OrExpression<T>( [ this, matcher ] );
}
AND_ALL( matchers:BooleanExpression<T>[] ):BooleanExpression<T>
{
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<T>[] ):BooleanExpression<T>
{
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<T> extends BooleanExpression<T>
{
private _matcher:BooleanExpression<T>;
constructor( matcher:BooleanExpression<T> )
{
super();
this._matcher = matcher;
}
evaluate( t:T )
{
return ! this._matcher.evaluate( t );
}
NOT()
{
return this._matcher;
}
}
export abstract class ListInputExpression<T> extends BooleanExpression<T>
{
protected _matchers:BooleanExpression<T>[];
constructor( matchers:BooleanExpression<T>[] )
{
super();
this._matchers = matchers;
}
}
export class AndExpression<T> extends ListInputExpression<T>
{
evaluate( t:T )
{
return this._matchers.every( m => m.evaluate( t ) );
}
AND( matcher:BooleanExpression<T> )
{
this._matchers.push( matcher );
return this;
}
}
export class OrExpression<T> extends ListInputExpression<T>
{
evaluate( t:T )
{
return this._matchers.some( m => m.evaluate( t ) );
}
OR( matcher:BooleanExpression<T> )
{
this._matchers.push( matcher );
return this;
}
}

View File

@ -0,0 +1,40 @@
import { BooleanExpression } from "./BooleanExpression";
export abstract class StringValueMatcher<T> extends BooleanExpression<T>
{
abstract getStringValue( t:T ):string;
}
export abstract class LitaralMatcher<T,L extends string> extends StringValueMatcher<T>
{
_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<T> extends StringValueMatcher<T>
{
private _regexp:RegExp;
constructor( regexp:RegExp )
{
super();
this._regexp = regexp;
}
evaluate( t:T )
{
let value = this.getStringValue( t );
return this._regexp.test( value );
}
}

115
browser/geometry/Range.ts Normal file
View File

@ -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 );
}
}

View File

@ -0,0 +1,48 @@
import { TreeWalker } from './TreeWalker';
export abstract class ArrayChildrenTreeWalker<T> extends TreeWalker<T>
{
_parentMap = new Map<T,T>();
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;
}
}

View File

@ -0,0 +1,42 @@
import { TreeWalker } from './TreeWalker';
export class HTMLNodeTreeWalker extends TreeWalker<Node>
{
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;
}
}

View File

@ -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<N>
{
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<numKids; i++ )
{
if ( this.childAt( p, i ) == node )
{
return i;
}
}
return -1;
}
siblingAt( node:N, index:number ):N
{
let p = this.parent( node );
if ( p == null || index<0 || index >= 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<N> ):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 );
}
}

283
browser/math/MathX.ts Normal file
View File

@ -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<T>( array:T[], currentIndex:number, direction:number )
{
let nextIndex = currentIndex + direction;
nextIndex = MathX.repeat( nextIndex, array.length );
return nextIndex;
}
static nextIndex<T>( array:T[], currentIndex:number )
{
return MathX.advanceIndex( array, currentIndex, 1 );
}
static previousIndex<T>( array:T[], currentIndex:number )
{
return MathX.advanceIndex( array, currentIndex, -1 );
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

73
browser/random/LCG.ts Normal file
View File

@ -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<LCGState>
{
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();
}
}
}

View File

@ -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<T>( array: T[], alternative:T = null ):T
{
if ( array.length == 0 )
{
return alternative;
}
var index = this.integerExclusive( 0, array.length );
return array[ index ];
}
arrayFrom<T>( numElements:number, array: T[], alternative:T = null ):T[]
{
let randomArray:T[] = [];
for ( let i = 0; i < numElements; i++ )
{
randomArray.push( this.from<T>( array, alternative ) );
}
return randomArray;
}
fromValues<T>( 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<T>( numElements:number, first:T, ...values:T[] ):T[]
{
let randomArray:T[] = [];
for ( let i = 0; i < numElements; i++ )
{
randomArray.push( this.from<T>( [first].concat( values ) ) );
}
return randomArray;
}
fromSet<T>( set:Set<T> )
{
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<S> extends RandomEngine
{
abstract setSeed( seed:number ):void;
abstract getSeedState():S;
abstract setSeedState( seedState:S ):void;
}

View File

@ -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;
}
}

View File

@ -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<any> = 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;
}
}

View File

@ -0,0 +1,7 @@
export abstract class ElementProcessor
{
abstract getElementName():string;
abstract processElement( element:Element ):Element;
hasPostProcessor(){ return false;}
postProcessElement( inputElement:Element, processedElement:Element ){}
}

View File

@ -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( "&amp;&amp;", "&" );
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;
}
*/
}

View File

@ -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;
}
}
}

View File

@ -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;
}
}

View File

@ -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<string,Element> = new Map<string,Element>();
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<string,ElementProcessor>();
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><body>${html}</body>` );
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 );
}
}

View File

@ -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 ) ;
}
}

View File

@ -0,0 +1,11 @@
import { StylesProcessorLexer } from "./StylesProcessorLexer";
export class StylesProcessor
{
static convert( styles:string )
{
let lexer = new StylesProcessorLexer();
return lexer.convert( styles );
}
}

View File

@ -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<string>();
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( "" );
}
}

View File

@ -0,0 +1,96 @@
import { RegExpUtility } from "./RegExpUtitlity";
export class ExtendedRegex
{
private static _extensions:Map<RegExp,string> = 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<RegExp,string>();
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;
}
}

108
browser/text/Levehshtein.ts Normal file
View File

@ -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 ];
}
}

View File

@ -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 =
/(<tag(?: (?:\w|\s|\-|\=|"(?:\.|(?:\\\")|[^\""\n])*"|'(?:\.|(?:\\\')|[^\''\n])*')*)?>)((?:.|\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<string,string> )
{
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<T>( list:T[], matching:string, getText:(t:T)=>string = null ):T
{
let index = this.getClosestIndex( list, matching, getText );
return list[ index ];
}
static getClosestIndex<T>( 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;
}
}

View File

@ -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
);
}
}

View File

@ -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
);
}
}

View File

@ -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 `<code-token-${t.type}>${escapedText}</code-token-${t.type}>`
}
);
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 `<code-token data-type="${t.type}">${escapedText}</code-token>`
}
);
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 = `<html><meta charset="utf-8"></meta><head><style>${styles}</style></head><body><pre>${htmlTags.join( "" )}</pre></body></html>`
return html;
}
}

170
browser/text/lexer/Lexer.ts Normal file
View File

@ -0,0 +1,170 @@
import { LexerMatcher } from "./LexerMatcher";
import { LexerEvent } from "./LexerEvent";
export class Lexer
{
static readonly defaultMode = "default";
private _modes = new Map<string,LexerMatcher[]>();
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<string> )
{
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;
}
}

View File

@ -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;
}
}

View File

@ -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<LexerEvent,string>
{
getStringValue( l:LexerEvent){return l.type;}
}
export class RegExpLexerEventTypeMatcher extends RegExpMatcher<LexerEvent>
{
getStringValue( l:LexerEvent){return l.type;}
}
export class LiteralLexerEventValueMatcher extends LitaralMatcher<LexerEvent,string>
{
getStringValue( l:LexerEvent){return l.match;}
}
export class RegExpLexerEventValueMatcher extends RegExpMatcher<LexerEvent>
{
getStringValue( l:LexerEvent){return l.match;}
}
export class LexerEventMatcherTools
{
static createTypeAndValueMatcher<Type extends LexerType>( type:Type, value:string )
{
let typeMatcher = new LiteralLexerEventTypeMatcher( type );
let valueMatcher = new LiteralLexerEventValueMatcher( value );
return typeMatcher.AND( valueMatcher );
}
static convertToExpression<Type extends LexerType>( value:BooleanExpression<LexerEvent>|Type[]|string ):BooleanExpression<LexerEvent>
{
if ( typeof value === "string" )
{
return new LiteralLexerEventValueMatcher( value )
}
else if ( Array.isArray( value ) )
{
let types = value as LexerType[];
let outputMatcher:BooleanExpression<LexerEvent> = 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<LexerEvent>;
}
static createMatchSequence<Type extends LexerType>( matcherDefinition:(BooleanExpression<LexerEvent>|Type[]|string)[],
ignoreDefinition:(BooleanExpression<LexerEvent>|Type[]|string)[] )
{
let matchers = matcherDefinition.map( md => LexerEventMatcherTools.convertToExpression<Type>( md ) );
let ignores = ignoreDefinition.map( id => LexerEventMatcherTools.convertToExpression<Type>( 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;
}
}

View File

@ -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;
}
}

View File

@ -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|\.|\-|\?|\=|\+|\/)+/ );
}

View File

@ -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, /./ );
}

View File

@ -0,0 +1,261 @@
import { BooleanExpression } from "../../expressions/BooleanExpression";
import { LexerEvent } from "./LexerEvent";
export class LexerQuery
{
source:string;
tokens:LexerEvent[];
_index:Map<LexerEvent,number> = new Map<LexerEvent,number>();
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<LexerEvent,string> )
{
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> ):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<LexerEvent>, end:BooleanExpression<LexerEvent>)
{
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<LexerEvent>, ignore?:BooleanExpression<LexerEvent>, 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<LexerEvent>, ignore?:BooleanExpression<LexerEvent>, maxItems:number = 100000 )
{
let nextIndex = this.searchIndex( index, forward, matcher, ignore );
return nextIndex === -1 ? null : this.tokens[ nextIndex ];
}
}

View File

@ -0,0 +1,88 @@
import { LexerEvent } from "./LexerEvent";
import { BooleanExpression } from "../../expressions/BooleanExpression";
import { LexerQuery } from "./LexerQuery";
export class LexerSequence
{
sequence:BooleanExpression<LexerEvent>[] = [];
ignore:BooleanExpression<LexerEvent> = 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;
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,8 @@
export class PHPClasses
{
static list:string[] =
[
"ReflectionClass",
"ReflectionNamedType"
]
}

View File

@ -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
);
}
}

View File

@ -0,0 +1,13 @@
import { SourceInfo } from "../parsing/SourceInfo";
import { ExpressionNode } from "./ExpressionNode";
export class BinaryExpression<T> extends ExpressionNode<T>
{
constructor( left:ExpressionNode<T>, right:ExpressionNode<T>, data:T, sourceInfo:SourceInfo )
{
super( [ left, right ], data, sourceInfo );
}
get left(){ return this._children[ 0 ]; }
get right(){ return this._children[ 1 ]; }
}

View File

@ -0,0 +1,15 @@
import { SourceInfo } from "../parsing/SourceInfo";
export class ExpressionNode<T>
{
_sourceInfo:SourceInfo;
_children:ExpressionNode<T>[];
_data:T;
constructor( children:ExpressionNode<T>[], data:T, sourceInfo:SourceInfo )
{
this._children = children;
this._data = data;
this._sourceInfo = sourceInfo;
}
}

View File

@ -0,0 +1,12 @@
import { SourceInfo } from "../parsing/SourceInfo";
import { ExpressionNode } from "./ExpressionNode";
export class LiteralNode<T> extends ExpressionNode<T>
{
constructor( literal:ExpressionNode<T>, data:T, sourceInfo:SourceInfo )
{
super( [ literal ], data, sourceInfo );
}
get value(){ return this._children[ 0 ] }
}

View File

@ -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"
]
}

View File

@ -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"
]
}

View File

@ -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"
]
}

View File

@ -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 )
)
}

View File

@ -0,0 +1,4 @@
export class OperatorResolver
{
}

View File

@ -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<any>[] = [];
_source:string;
get source(){ return this._source; }
_messages:ParserMessage[] = [];
get messages():ParserMessage[]{ return this._messages }
_add( phase:ParserPhase<any> )
{
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" );
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,21 @@
import { Parser } from "./Parser";
export abstract class ParserPhase<P extends Parser>
{
#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;
}

View File

@ -0,0 +1,7 @@
import { OperatorResolver } from "./OperatorResolver";
export class PrecedenceLevel
{
fromLeft:boolean = true;
operatorResolver:OperatorResolver[];
}

View File

@ -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 );
}
}

View File

@ -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 );
}
}

View File

@ -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 );
}
}

View File

@ -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 = /(<TAG\zATTRIBUTE\s*=\s*"(.*)"\z>)(\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</${innerTag}>$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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

179
browser/tools/Arrays.ts Normal file
View File

@ -0,0 +1,179 @@
export class Arrays
{
static combine<T>( t:T, array:T[] ):T[]
{
let combined = [ t ];
if ( array )
{
combined = combined.concat( array );
}
return combined;
}
static combine2<T>( t:T, t2:T, array:T[] ):T[]
{
let combined = [ t, t2 ];
if ( array )
{
combined = combined.concat( array );
}
return combined;
}
static combine3<T>( t:T, t2:T, t3:T, array:T[] ):T[]
{
let combined = [ t, t2, t3 ];
if ( array )
{
combined = combined.concat( array );
}
return combined;
}
static pushIfNotInside<T>( array:T[], element:T )
{
let index = array.indexOf( element );
if ( index !== - 1)
{
return;
}
array.push( element );
}
static create<T>( 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<T>( source:T[] )
{
return source.length === 0;
}
static isLast<T>( source:T[], index:number )
{
return index === ( source.length -1 ) ;
}
static getLast<T>( source:T[] ):T
{
return source[ source.length - 1 ];
}
static forEachIndex<T>( 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<T>( source:T[], target:T[] )
{
this.transferRange( source, 0, target, 0, source.length );
}
static transferRange<T>( 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<T>( array:T[], start:number, length:number )
{
return array.slice( start, start + length );
}
static copyRangeFromStart<T>( array:T[], start:number )
{
return array.slice( start, array.length );
}
static insert<T>( array:T[], element:T, index:number )
{
array.splice( index, 0, element );
}
static moveToFront<T>( array:T[], element:T )
{
if ( array[ 0 ] === element )
{
return;
}
Arrays.remove( array, element );
Arrays.insert( array, element, 0 );
}
static remove<T>( array:T[], element:T )
{
if ( ! array || ! element )
{
return;
}
let index = array.indexOf( element );
if ( index === -1 )
{
return;
}
array.splice( index, 1 );
}
static addIfNotPresent<T>( array:T[], t:T)
{
let index = array.indexOf( t );
if ( index !== -1 )
{
return;
}
array.push( t );
}
static removeAt<T>( 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 );
}
}

20
browser/tsconfig.json Normal file
View File

@ -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/"],
}
}

7
node/tsconfig.json Normal file
View File

@ -0,0 +1,7 @@
{
"compilerOptions": {
"lib": ["ES2020"],
"types": ["node"], // Includes Node.js types
},
"include": ["./*"]
}