11// const WHITESPACE = /^[ \t]+/;
22// const NEWLINE = /^[;\n]+/;
3- const WORDSEP = / ^ [ \t ; \n ] + / ;
3+ const WORDSEP = / ^ [ \t \n \r \f \v ] + / ;
44const QUOTED = / ^ " .* ?[ ^ \\ ] " / ;
5- const WORD = / ^ ( [ ^ \t ; \n { } [ \] " \\ ] | \\ [ \t ; \n { } [ \] " \\ ] ) + / ;
6- const ESCAPED = / \\ ( .) / g;
7- const ESCAPABLE = / ( [ \t ; \n { } [ \] " ] ) / g;
5+ const WORD = / ^ ( [ ^ \t \n \r \f \v { } \\ ] | \\ .) + / ;
86
97const eat = ( regex , str ) => {
108 const match = str . match ( regex ) ;
@@ -34,26 +32,84 @@ const eatBrace = (str) => {
3432} ;
3533
3634const checkBrace = ( str ) => {
37- let bc = 0 ;
38- let bk = 0 ;
35+ let curlies = 0 ;
36+ let brackets = 0 ;
3937 const l = str . length ;
4038 for ( let i = 0 ; i < l ; i ++ ) {
4139 switch ( str [ i ] ) {
42- case '{' : bc ++ ; break ;
43- case '}' : bc -- ; break ;
44- case '[' : bk ++ ; break ;
45- case ']' : bk -- ; break ;
40+ case '{' : curlies ++ ; break ;
41+ case '}' : curlies -- ; break ;
42+ case '[' : brackets ++ ; break ;
43+ case ']' : brackets -- ; break ;
44+ }
45+
46+ if ( curlies < 0 ) return false ;
47+ }
48+
49+ return curlies === 0 && brackets >= 0 ;
50+ } ;
51+
52+ // ported from JimEscape in jim.c
53+ const UNESCAPE_TABLE = {
54+ 'a' : '\a' ,
55+ 'b' : '\b' ,
56+ 'n' : '\n' ,
57+ 'r' : '\r' ,
58+ 't' : '\t' ,
59+ 'f' : '\f' ,
60+ 'v' : '\v' ,
61+ '0' : '\x00' ,
62+ } ;
63+
64+ const unescape = ( str ) => {
65+ let output = "" ;
66+
67+ for ( let i = 0 ; i < str . length ; i ++ ) {
68+ // used to prevent out-of-bounds issues with `str[i + 1]`
69+ if ( i === str . length - 1 ) {
70+ output += str [ i ] ;
71+ break ;
4672 }
4773
48- if ( bc < 0 ) return false ;
49- if ( bk < 0 ) return false ;
74+ if ( str [ i ] === '\\' ) {
75+ // is the next character escapable?
76+ const unescaped = UNESCAPE_TABLE [ str [ i + 1 ] ] ;
77+ if ( unescaped ) {
78+ output += unescaped ;
79+ } else {
80+ // else just append it, sans the backslash
81+ output += str [ i + 1 ] ;
82+ }
83+
84+ i ++ ;
85+ // TODO: octal, \x \u \U \u{NNN}
86+ } else {
87+ output += str [ i ] ;
88+ }
5089 }
5190
52- return bc === 0 && bk === 0 ;
91+ return output ;
92+ } ;
93+
94+ // ported from BackslashQuoteString in jim.c
95+ const ESCAPE_TABLE = {
96+ ' ' : '\\ ' ,
97+ '$' : '\\$' ,
98+ '"' : '\\"' ,
99+ '[' : '\\[' ,
100+ ']' : '\\]' ,
101+ '{' : '\\{' ,
102+ '}' : '\\}' ,
103+ ';' : '\\;' ,
104+ '\\' : '\\\\' ,
105+ '\n' : '\\n' ,
106+ '\r' : '\\r' ,
107+ '\t' : '\\t' ,
108+ '\f' : '\\f' ,
109+ '\v' : '\\v' ,
53110} ;
54111
55- const unescape = ( str ) => str . replaceAll ( ESCAPED , '$1' ) ;
56- const escape = ( str ) => str . replaceAll ( ESCAPABLE , '\\$1' ) ;
112+ const escape = ( str ) => str . split ( "" ) . map ( char => char in ESCAPE_TABLE ? ESCAPE_TABLE [ char ] : char ) . join ( "" ) ;
57113
58114// deserializtion
59115const nextToken = ( str ) => {
@@ -69,14 +125,19 @@ const nextToken = (str) => {
69125 case ' ' :
70126 case '\t' :
71127 case '\n' :
72- case ';' :
128+ case '\r' :
129+ case '\f' :
130+ case '\v' :
73131 return [ token , str ] ;
74132
75- case '[' :
76- throw new Error ( "this aint no interpreter" ) ;
77133 case '{' :
78134 [ word , str ] = eatBrace ( str ) ;
79- token += word . slice ( 1 , - 1 ) ;
135+
136+ if ( token === '' ) {
137+ token = word . slice ( 1 , - 1 ) ;
138+ } else {
139+ token += word ;
140+ }
80141 break ;
81142 case '"' :
82143 [ word , str ] = eat ( QUOTED , str ) ;
@@ -127,8 +188,13 @@ const loadDict = (str) => {
127188const dumpString = ( word ) => {
128189 if ( ! word . length ) return '{}' ;
129190 const match = word . match ( WORD ) ;
130- if ( match && match [ 0 ] === word ) return word ;
131- if ( checkBrace ( word ) ) return `{${ word } }` ;
191+ if ( word [ 0 ] !== "#" && match && match [ 0 ] === word ) return word ;
192+ if ( checkBrace ( word ) ) {
193+ // the one case that we can't handle with braces is a linefeed escape
194+ if ( ! ( / \\ \n / . test ( word ) ) ) {
195+ return `{${ word } }` ;
196+ }
197+ }
132198 return escape ( word ) ;
133199}
134200
@@ -280,7 +346,7 @@ class FolkWS {
280346 const channel = this . createChannel ( ( message ) => {
281347 const [ action , match , matchId ] = loadList ( message ) ;
282348 const callback = callbacks [ action ] ;
283- callback && callback ( loadDict ( match ) , matchId ) ;
349+ if ( callback ) callback ( loadDict ( match ) , matchId ) ;
284350 } ) ;
285351
286352 const retractKey = await this . send ( tcl `
0 commit comments