├── tests ├── data │ ├── files │ │ ├── countMe.txt │ │ ├── dateMe.txt │ │ ├── createMe.txt │ │ ├── loopMe │ │ │ ├── file1.txt │ │ │ ├── file2.txt │ │ │ ├── dir1 │ │ │ │ └── subfile1.txt │ │ │ └── dir2 │ │ │ │ └── subfile1.txt │ │ ├── test.json │ │ ├── writeMe.txt │ │ ├── fileNames │ │ │ ├── abc def.txt │ │ │ ├── abc"def.txt │ │ │ ├── abc%def.txt │ │ │ ├── abc,def.txt │ │ │ ├── abc.def.txt │ │ │ ├── abc:def.txt │ │ │ ├── abc;def.txt │ │ │ ├── abc=def.txt │ │ │ ├── abc?def.txt │ │ │ ├── abc|def.txt │ │ │ ├── abc" "def.txt │ │ │ ├── abc(def).txt │ │ │ ├── abc.txt │ │ │ └── abc_backslash_def.txt │ │ ├── testMime.txtx │ │ ├── copyMe │ │ │ └── subDir │ │ │ │ └── file1.txt │ │ ├── moveMe │ │ │ └── subDir │ │ │ │ └── file1.txt │ │ ├── appendMe.txt │ │ ├── multiline.txt │ │ ├── truncateMeLines.txt │ │ ├── gd │ │ │ ├── frog.png │ │ │ ├── favicon.png │ │ │ ├── gradient.png │ │ │ ├── color_bands.jpg │ │ │ ├── landscape.jpeg │ │ │ ├── logo_social.png │ │ │ ├── test_font.ttf │ │ │ ├── motorcycle_alpha.webp │ │ │ └── oscar_the_grouch_thumb200.jpg │ │ ├── testMime.pdfx │ │ └── truncateMe.txt │ └── logs │ │ ├── contact.txt │ │ └── app.log ├── code │ ├── pages │ │ ├── errors │ │ │ ├── outside-loop.tht │ │ │ ├── file-too-big.tht │ │ │ ├── format-checker-space.tht │ │ │ ├── arg-error.tht │ │ │ ├── missing-close-brace.tht │ │ │ ├── send-404.tht │ │ │ ├── arg-error-boolean.tht │ │ │ ├── arg-error-not-enough.tht │ │ │ ├── bad-keyword.tht │ │ │ ├── blank-file.tht │ │ │ ├── die.tht │ │ │ ├── unknown-method-module.tht │ │ │ ├── dupe-fun-arg.tht │ │ │ ├── index-out-of-bounds.tht │ │ │ ├── function-not-fun.tht │ │ │ ├── missing-tem-suffix.tht │ │ │ ├── out-of-scope.tht │ │ │ ├── unsupported-require.tht │ │ │ ├── for-not-foreach.tht │ │ │ ├── missing-dollar-var.tht │ │ │ ├── format-checker-var-case.tht │ │ │ ├── unknown-method-class.tht │ │ │ ├── arg-error-too-many.tht │ │ │ ├── call-php.tht │ │ │ ├── format-checker-html.tht │ │ │ ├── fun-redefined.tht │ │ │ ├── error-with-print.tht │ │ │ ├── adjacent-tokens.tht │ │ │ ├── max-run-time.tht │ │ │ ├── unsupported-keyword.tht │ │ │ ├── max-memory.tht │ │ │ ├── unknown-method-module-suggest.tht │ │ │ ├── -index.tht │ │ │ └── -todo.tht │ │ ├── home.tht │ │ ├── default.tht │ │ ├── benchmark │ │ │ ├── hello.tht │ │ │ └── test-route.tht │ │ ├── misc │ │ │ ├── json-output.tht │ │ │ ├── email.tht │ │ │ ├── -index.tht │ │ │ ├── print.tht │ │ │ ├── form-login.tht │ │ │ ├── form-upload.tht │ │ │ ├── form-upload-image.tht │ │ │ ├── image-optim.tht │ │ │ └── form-checkboxes.tht │ │ └── user-profile.tht │ ├── public │ │ ├── uploads │ │ ├── favicon.ico │ │ ├── favicon.png │ │ ├── images │ │ │ ├── icon.png │ │ │ ├── image.png │ │ │ └── optimize │ │ │ │ ├── text.png │ │ │ │ ├── gradient.png │ │ │ │ ├── bg_texture.png │ │ │ │ ├── landscape.jpeg │ │ │ │ ├── logo_social.png │ │ │ │ ├── tht_favicon.png │ │ │ │ ├── text_thumb200.webp │ │ │ │ ├── motorcycle_alpha.png │ │ │ │ ├── oscar_the_grouch.png │ │ │ │ ├── text_optimized.webp │ │ │ │ ├── gradient_optimized.webp │ │ │ │ ├── gradient_thumb200.webp │ │ │ │ ├── landscape_thumb200.webp │ │ │ │ ├── bg_texture_optimized.webp │ │ │ │ ├── landscape_optimized.webp │ │ │ │ ├── logo_social_optimized.webp │ │ │ │ ├── logo_social_thumb200.webp │ │ │ │ ├── tht_favicon_optimized.webp │ │ │ │ ├── tht_favicon_thumb200.webp │ │ │ │ ├── lospec_continuum_773674.png │ │ │ │ ├── motorcycle_alpha_optimized.webp │ │ │ │ ├── motorcycle_alpha_thumb200.webp │ │ │ │ ├── oscar_the_grouch_optimized.webp │ │ │ │ ├── oscar_the_grouch_thumb200.webp │ │ │ │ ├── lospec_continuum_773674_optimized.webp │ │ │ │ └── lospec_continuum_773674_thumb200.webp │ │ ├── front.php │ │ ├── benchmark │ │ │ └── php_test_route.php │ │ ├── test_perf_issue.php │ │ └── .htaccess │ ├── php │ │ ├── PhpModule.php │ │ └── vendor │ │ │ └── testVendorClass.php │ └── modules │ │ ├── oop │ │ ├── ComposeEmbeddedConflict.tht │ │ ├── ComposeHelper.tht │ │ ├── FactoryObject.tht │ │ ├── ComposeEmbeddedOther.tht │ │ ├── ComposeEmbedded.tht │ │ ├── ComposeParent.tht │ │ └── BaseObject.tht │ │ ├── subDir │ │ ├── AdjacentModule.tht │ │ └── OtherModule.tht │ │ ├── tests │ │ ├── lib │ │ │ ├── OutputTests.tht │ │ │ ├── ResultTests.tht │ │ │ ├── CookieTests.tht │ │ │ ├── ImageTests.tht │ │ │ ├── AppConfigTests.tht │ │ │ ├── LogTests.tht │ │ │ ├── PasswordTests.tht │ │ │ ├── RequestTests.tht │ │ │ ├── MathTests.tht │ │ │ ├── SystemTests.tht │ │ │ ├── NetTests.tht │ │ │ ├── PerfTests.tht │ │ │ ├── SessionTests.tht │ │ │ ├── MetaTests.tht │ │ │ ├── FormTests.tht │ │ │ ├── WebTests.tht │ │ │ ├── JconTests.tht │ │ │ ├── PhpTests.tht │ │ │ ├── PageTests.tht │ │ │ ├── JsonTests.tht │ │ │ └── CacheTests.tht │ │ ├── lang │ │ │ ├── FlagTests.tht │ │ │ ├── BitwiseTests.tht │ │ │ ├── TypeHints.tht │ │ │ ├── TypeStringTests.tht │ │ │ ├── TypeTests.tht │ │ │ ├── LoopTests.tht │ │ │ └── UserModuleTests.tht │ │ └── errors │ │ │ └── RuntimeErrorTests.tht │ │ └── TestModule.tht ├── readme.md └── config │ ├── app.jcon │ └── app.local.jcon ├── tht ├── run │ ├── tht.cmd │ └── tht.php └── lib │ ├── core │ ├── data │ │ └── starterApp │ │ │ ├── code │ │ │ ├── pages │ │ │ │ ├── examples │ │ │ │ │ ├── hello.tht │ │ │ │ │ ├── hello-html.tht │ │ │ │ │ ├── hello-query.tht │ │ │ │ │ ├── hello-page.tht │ │ │ │ │ ├── route-colors.tht │ │ │ │ │ ├── hello-form.tht │ │ │ │ │ ├── upload-form.tht │ │ │ │ │ ├── checkbox-form.tht │ │ │ │ │ ├── ajax-weather.tht │ │ │ │ │ ├── contact-form.tht │ │ │ │ │ └── database.tht │ │ │ │ └── home.tht │ │ │ ├── public │ │ │ │ ├── front.php │ │ │ │ ├── .htaccess │ │ │ │ └── css │ │ │ │ │ └── app.css │ │ │ └── modules │ │ │ │ └── App.tht │ │ │ └── config │ │ │ ├── app.jcon │ │ │ └── app.local.jcon │ ├── compiler │ │ ├── Symbol │ │ │ └── Symbols │ │ │ │ ├── S_Statement.php │ │ │ │ ├── S_Template.php │ │ │ │ ├── S_TemplateString.php │ │ │ │ ├── S_Prefix.php │ │ │ │ ├── S_Separator.php │ │ │ │ ├── S_PreKeyword.php │ │ │ │ ├── S_Command.php │ │ │ │ ├── S_ClassFields.php │ │ │ │ ├── S_Loop.php │ │ │ │ ├── S_TemplateExpr.php │ │ │ │ ├── S_ShortPrint.php │ │ │ │ ├── S_Return.php │ │ │ │ ├── S_TryCatch.php │ │ │ │ ├── S_ClassPlugin.php │ │ │ │ ├── S_Dot.php │ │ │ │ ├── S_New.php │ │ │ │ ├── S_Lambda.php │ │ │ │ ├── S_OpenSquare.php │ │ │ │ ├── S_If.php │ │ │ │ ├── S_Ternary.php │ │ │ │ ├── S_InfixSticky.php │ │ │ │ ├── S_Class.php │ │ │ │ ├── S_ForEach.php │ │ │ │ ├── S_Unsupported.php │ │ │ │ ├── S_Literal.php │ │ │ │ ├── S_OpenParen.php │ │ │ │ ├── S_OpenCurly.php │ │ │ │ └── S_Match.php │ │ ├── templateTransformers │ │ │ ├── JconTemplateTransformer.php │ │ │ ├── TextTemplateTransformer.php │ │ │ ├── JsTemplateTransformer.php │ │ │ ├── CssTemplateTransformer.php │ │ │ └── LmTemplateTransformer.php │ │ ├── SourceAnalyzer │ │ │ └── SourceMap.php │ │ ├── _index.php │ │ ├── Tokenizer │ │ │ └── TokenStream.php │ │ └── TemplateTransformers │ │ │ └── TemplateTransformers.php │ └── main │ │ ├── Error │ │ ├── ErrorPage │ │ │ ├── components │ │ │ │ ├── ErrorPageTitle.php │ │ │ │ ├── ErrorPageMessage.php │ │ │ │ ├── ErrorPageFilePath.php │ │ │ │ ├── ErrorPageComponent.php │ │ │ │ └── ErrorPageObjectDetail.php │ │ │ └── ErrorPageText.php │ │ └── ErrorHandlerMinimal.php │ │ └── Tht │ │ ├── ThtInit.php │ │ └── ThtErrors.php │ └── stdlib │ ├── modules │ ├── Image │ │ └── resources │ │ │ └── ibm_plex_condensed_min.ttf │ ├── _index.php │ ├── Result.php │ ├── Form.php │ ├── Cookie.php │ ├── AppConfig.php │ ├── Bare.php │ ├── Input │ │ └── InputValidatorRuleParser.php │ └── String.php │ └── classes │ ├── OFunction.php │ ├── OBoolean.php │ ├── OVar.php │ ├── ONull.php │ ├── _index.php │ ├── OPassword.php │ ├── ORegex.php │ ├── OTypeString │ └── OCore.php │ └── OStdModule.php ├── readme.md ├── .gitignore └── LICENSE /tests/data/files/countMe.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/files/dateMe.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/files/createMe.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/files/loopMe/file1.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/files/loopMe/file2.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/files/test.json: -------------------------------------------------------------------------------- 1 | {"a" -------------------------------------------------------------------------------- /tests/data/files/writeMe.txt: -------------------------------------------------------------------------------- 1 | zzz -------------------------------------------------------------------------------- /tests/data/files/fileNames/abc def.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/files/fileNames/abc"def.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/files/fileNames/abc%def.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/files/fileNames/abc,def.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/files/fileNames/abc.def.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/files/fileNames/abc:def.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/files/fileNames/abc;def.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/files/fileNames/abc=def.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/files/fileNames/abc?def.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/files/fileNames/abc|def.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/files/testMime.txtx: -------------------------------------------------------------------------------- 1 | text -------------------------------------------------------------------------------- /tests/data/files/copyMe/subDir/file1.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/files/fileNames/abc" "def.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/files/fileNames/abc(def).txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/files/fileNames/abc.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/files/loopMe/dir1/subfile1.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/files/loopMe/dir2/subfile1.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/files/moveMe/subDir/file1.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/files/fileNames/abc_backslash_def.txt: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/data/files/appendMe.txt: -------------------------------------------------------------------------------- 1 | 123456789 2 | 000 3 | -------------------------------------------------------------------------------- /tht/run/tht.cmd: -------------------------------------------------------------------------------- 1 | @php "%~dp0tht.php" %* 2 | 3 | -------------------------------------------------------------------------------- /tests/code/pages/errors/outside-loop.tht: -------------------------------------------------------------------------------- 1 | 2 | continue 3 | -------------------------------------------------------------------------------- /tests/code/pages/errors/file-too-big.tht: -------------------------------------------------------------------------------- 1 | 2 | // TODO 3 | 4 | -------------------------------------------------------------------------------- /tests/code/pages/errors/format-checker-space.tht: -------------------------------------------------------------------------------- 1 | 2 | $a=1 3 | -------------------------------------------------------------------------------- /tests/code/pages/errors/arg-error.tht: -------------------------------------------------------------------------------- 1 | 2 | q[a b c].join([]) 3 | -------------------------------------------------------------------------------- /tests/data/files/multiline.txt: -------------------------------------------------------------------------------- 1 | 11 2 | 22 3 | 33 4 | 5 | 6 | 100 -------------------------------------------------------------------------------- /tests/code/pages/errors/missing-close-brace.tht: -------------------------------------------------------------------------------- 1 | 2 | if true { 3 | 4 | -------------------------------------------------------------------------------- /tests/code/pages/home.tht: -------------------------------------------------------------------------------- 1 | 2 | Output.redirect(url'/all-tests') 3 | 4 | -------------------------------------------------------------------------------- /tests/code/pages/errors/send-404.tht: -------------------------------------------------------------------------------- 1 | 2 | // 404 3 | Output.sendError(404) 4 | -------------------------------------------------------------------------------- /tests/code/pages/errors/arg-error-boolean.tht: -------------------------------------------------------------------------------- 1 | 2 | q[a b c].join(true) 3 | 4 | -------------------------------------------------------------------------------- /tests/code/pages/errors/arg-error-not-enough.tht: -------------------------------------------------------------------------------- 1 | 2 | Json.encode() 3 | 4 | 5 | -------------------------------------------------------------------------------- /tests/code/pages/errors/bad-keyword.tht: -------------------------------------------------------------------------------- 1 | 2 | func foo { 3 | // ... 4 | } 5 | -------------------------------------------------------------------------------- /tests/code/pages/errors/blank-file.tht: -------------------------------------------------------------------------------- 1 | 2 | // Intentionally left blank 3 | 4 | -------------------------------------------------------------------------------- /tests/code/pages/errors/die.tht: -------------------------------------------------------------------------------- 1 | 2 | fun main { 3 | die('Custom Error') 4 | } -------------------------------------------------------------------------------- /tests/code/pages/errors/unknown-method-module.tht: -------------------------------------------------------------------------------- 1 | 2 | Json.sdfsdf() 3 | 4 | 5 | -------------------------------------------------------------------------------- /tests/code/pages/errors/dupe-fun-arg.tht: -------------------------------------------------------------------------------- 1 | 2 | fun foo($aa, $aa) { 3 | // ... 4 | } 5 | -------------------------------------------------------------------------------- /tests/code/pages/errors/index-out-of-bounds.tht: -------------------------------------------------------------------------------- 1 | 2 | $a = [1, 2, 3] 3 | $a[10] = 'x' 4 | 5 | -------------------------------------------------------------------------------- /tests/data/files/truncateMeLines.txt: -------------------------------------------------------------------------------- 1 | xxxxxx 2 | xxxxxx 3 | xxxxxx 4 | xxxxxx 5 | xxxxxx 6 | -------------------------------------------------------------------------------- /tht/lib/core/data/starterApp/code/pages/examples/hello.tht: -------------------------------------------------------------------------------- 1 | 2 | print('Hello World!') 3 | -------------------------------------------------------------------------------- /tests/code/pages/errors/function-not-fun.tht: -------------------------------------------------------------------------------- 1 | 2 | function foo($aa) { 3 | // ... 4 | } 5 | -------------------------------------------------------------------------------- /tests/code/pages/errors/missing-tem-suffix.tht: -------------------------------------------------------------------------------- 1 | 2 | 3 | tem foo { 4 | // ... 5 | } 6 | 7 | -------------------------------------------------------------------------------- /tests/code/pages/errors/out-of-scope.tht: -------------------------------------------------------------------------------- 1 | 2 | $a = 3 3 | if false { 4 | print($b) 5 | } 6 | -------------------------------------------------------------------------------- /tests/code/pages/errors/unsupported-require.tht: -------------------------------------------------------------------------------- 1 | 2 | 3 | require('some/file.tht') 4 | 5 | 6 | -------------------------------------------------------------------------------- /tests/code/public/uploads: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joelesko/tht/HEAD/tests/code/public/uploads -------------------------------------------------------------------------------- /tests/code/pages/errors/for-not-foreach.tht: -------------------------------------------------------------------------------- 1 | 2 | for $a = 1; $a < 10; $a++ { 3 | // ... 4 | } 5 | -------------------------------------------------------------------------------- /tests/code/pages/errors/missing-dollar-var.tht: -------------------------------------------------------------------------------- 1 | 2 | 3 | if foobar == 999 { 4 | // ... 5 | } 6 | -------------------------------------------------------------------------------- /tests/code/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joelesko/tht/HEAD/tests/code/public/favicon.ico -------------------------------------------------------------------------------- /tests/code/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joelesko/tht/HEAD/tests/code/public/favicon.png -------------------------------------------------------------------------------- /tests/data/files/gd/frog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joelesko/tht/HEAD/tests/data/files/gd/frog.png -------------------------------------------------------------------------------- /tests/data/files/testMime.pdfx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joelesko/tht/HEAD/tests/data/files/testMime.pdfx -------------------------------------------------------------------------------- /tests/data/files/gd/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joelesko/tht/HEAD/tests/data/files/gd/favicon.png -------------------------------------------------------------------------------- /tests/data/files/gd/gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joelesko/tht/HEAD/tests/data/files/gd/gradient.png -------------------------------------------------------------------------------- /tests/code/pages/default.tht: -------------------------------------------------------------------------------- 1 | 2 | 3 | print('Default Page Controller:', Request.getUrl().getPathParts()) 4 | 5 | -------------------------------------------------------------------------------- /tests/code/pages/errors/format-checker-var-case.tht: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | fun getFlag($fl) { 5 | 6 | $aVAR = 1 7 | 8 | } -------------------------------------------------------------------------------- /tests/code/public/images/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joelesko/tht/HEAD/tests/code/public/images/icon.png -------------------------------------------------------------------------------- /tests/code/public/images/image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joelesko/tht/HEAD/tests/code/public/images/image.png -------------------------------------------------------------------------------- /tests/data/files/gd/color_bands.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joelesko/tht/HEAD/tests/data/files/gd/color_bands.jpg -------------------------------------------------------------------------------- /tests/data/files/gd/landscape.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joelesko/tht/HEAD/tests/data/files/gd/landscape.jpeg -------------------------------------------------------------------------------- /tests/data/files/gd/logo_social.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joelesko/tht/HEAD/tests/data/files/gd/logo_social.png -------------------------------------------------------------------------------- /tests/data/files/gd/test_font.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joelesko/tht/HEAD/tests/data/files/gd/test_font.ttf -------------------------------------------------------------------------------- /tests/code/pages/errors/unknown-method-class.tht: -------------------------------------------------------------------------------- 1 | 2 | 3 | $str = 'string' 4 | 5 | >> $str.upperCase() 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/code/php/PhpModule.php: -------------------------------------------------------------------------------- 1 | > $a 6 | } 7 | 8 | 9 | -------------------------------------------------------------------------------- /tests/code/pages/errors/call-php.tht: -------------------------------------------------------------------------------- 1 | fun main { 2 | Php.require('PhpModule.php') 3 | Php.call('runtime_error') 4 | } 5 | -------------------------------------------------------------------------------- /tests/code/pages/errors/format-checker-html.tht: -------------------------------------------------------------------------------- 1 | 2 | tem html { 3 |
sdfsdf 4 | } 5 | 6 | -------------------------------------------------------------------------------- /tests/code/public/images/optimize/text.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joelesko/tht/HEAD/tests/code/public/images/optimize/text.png -------------------------------------------------------------------------------- /tests/data/files/gd/motorcycle_alpha.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joelesko/tht/HEAD/tests/data/files/gd/motorcycle_alpha.webp -------------------------------------------------------------------------------- /tests/data/files/truncateMe.txt: -------------------------------------------------------------------------------- 1 | xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx -------------------------------------------------------------------------------- /tests/code/pages/errors/fun-redefined.tht: -------------------------------------------------------------------------------- 1 | 2 | fun foo { 3 | // ... 4 | } 5 | 6 | fun foO { 7 | // ... 8 | } 9 | 10 | -------------------------------------------------------------------------------- /tests/code/pages/errors/error-with-print.tht: -------------------------------------------------------------------------------- 1 | 2 | 3 | $a = [] 4 | 5 | >> 'This is a print' 6 | 7 | $a = $a * 1 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /tests/code/public/images/optimize/gradient.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/joelesko/tht/HEAD/tests/code/public/images/optimize/gradient.png -------------------------------------------------------------------------------- /tests/code/public/front.php: -------------------------------------------------------------------------------- 1 | next(); 12 | return $this; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # THT 2 | 3 | **THT is a programming language for server-side web development.** 4 | 5 | Please visit **[tht.dev](https://tht.dev)**. 6 | 7 | 8 | ## Bugs & Feedback 9 | 10 | https://github.com/joelesko/tht/issues 11 | 12 | 13 | ## License 14 | 15 | THT is released under the [MIT License](https://opensource.org/licenses/MIT). 16 | 17 | -------------------------------------------------------------------------------- /tht/lib/core/compiler/Symbol/Symbols/S_Prefix.php: -------------------------------------------------------------------------------- 1 | next(); 11 | $this->space('*!x'); 12 | $this->addKids([$p->parseExpression(70)]); 13 | return $this; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tht/lib/stdlib/classes/OFunction.php: -------------------------------------------------------------------------------- 1 | toObjectString(); 10 | } 11 | 12 | public function __toString() { 13 | return $this->toObjectString(); 14 | } 15 | } 16 | 17 | -------------------------------------------------------------------------------- /tht/lib/core/data/starterApp/code/pages/examples/hello-html.tht: -------------------------------------------------------------------------------- 1 | 2 | $html = helloHtml() 3 | 4 | Output.sendHtml($html) 5 | 6 | tem helloHtml { 7 | 8 |
9 |

Hello World 10 | 11 |

This is an example of sending HTML directly to the browser. 12 | 13 | } 14 | -------------------------------------------------------------------------------- /tht/lib/core/compiler/templateTransformers/JsTemplateTransformer.php: -------------------------------------------------------------------------------- 1 | minifyJs($str); 11 | } 12 | 13 | return $str; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/code/public/benchmark/php_test_route.php: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | Route Param Test 13 | 14 | 15 | Route Param: 16 | 17 | -------------------------------------------------------------------------------- /tests/readme.md: -------------------------------------------------------------------------------- 1 | # Test Site 2 | 3 | This is a standalone site for running the THT unit tests. 4 | 5 | ## To run the test site: 6 | 7 | 1. `cd` to the `public` directory 8 | 2. Run `tht server` 9 | 3. Load http://localhost:3333 10 | 11 | 12 | ## To edit tests: 13 | 14 | The test page is currently at `pages/allTests.tht`. 15 | 16 | Tests are written in THT using the built-in `Test` module. 17 | 18 | -------------------------------------------------------------------------------- /tht/lib/core/compiler/Symbol/Symbols/S_Separator.php: -------------------------------------------------------------------------------- 1 | next(); 13 | 14 | return null; 15 | } 16 | } 17 | 18 | class S_End extends S_Separator { 19 | 20 | var $type = SymbolType::END; 21 | } 22 | -------------------------------------------------------------------------------- /tests/code/pages/errors/unknown-method-module-suggest.tht: -------------------------------------------------------------------------------- 1 | 2 | fun main { 3 | transformImage() 4 | } 5 | 6 | fun transformImage($image = '') { 7 | getColor('default') 8 | } 9 | 10 | fun getColor($filter) { 11 | 12 | $color = Math.hexdec('#66ff00') 13 | 14 | if $filter == 'monochrome' { 15 | $color = toMonochrome($color) 16 | } 17 | 18 | return $color 19 | } 20 | 21 | fun toMonochrome { 22 | return 0 23 | } 24 | -------------------------------------------------------------------------------- /tests/code/modules/tests/lang/FlagTests.tht: -------------------------------------------------------------------------------- 1 | 2 | public fun run($t) { 3 | 4 | $t.section('Flags') 5 | 6 | $flag = -myFlag 7 | $t.ok($flag.myFlag, 'flag - key') 8 | 9 | $t.ok(getFlag().myFlag == false, 'flag as arg - false') 10 | $t.ok(getFlag(-myFlag).myFlag == true, 'flag as arg - true') 11 | 12 | return $t 13 | } 14 | 15 | fun getFlag($fl = {}) { 16 | 17 | $fl.check({ 18 | myFlag: false 19 | }) 20 | 21 | return $fl 22 | } 23 | -------------------------------------------------------------------------------- /tests/code/modules/TestModule.tht: -------------------------------------------------------------------------------- 1 | 2 | @@.moduleVar = 'mod' 3 | 4 | @@.ModuleConstant = 'constant' 5 | 6 | @@.ConstantMap = { Red, Green, Blue } 7 | 8 | public fun changeConstantAndDie { 9 | @@.ModuleConstant = 'another' 10 | } 11 | 12 | public fun bareFun($name) { 13 | return 'bareFunction:' ~ $name 14 | } 15 | 16 | public fun testModuleVar { 17 | return 'moduleVar:' ~ @@.moduleVar 18 | } 19 | 20 | fun nonExportedFn { 21 | return 'non-exported' 22 | } 23 | 24 | -------------------------------------------------------------------------------- /tests/code/modules/tests/lib/ResultTests.tht: -------------------------------------------------------------------------------- 1 | public fun run($t) { 2 | 3 | $t.section('Result Objects') 4 | 5 | $okResult = Result.ok(123) 6 | $t.ok(!$okResult.getFailCode(), 'not ok') 7 | $t.ok($okResult.get() == 123, 'ok value') 8 | 9 | $failResult = Result.fail('testFail') 10 | $t.ok($failResult.getFailCode() == 'testFail', 'failCode') 11 | 12 | $t.dies(fun { $failResult.get() }, 'get failed result', 'in a failure state') 13 | 14 | return $t 15 | } -------------------------------------------------------------------------------- /tests/code/modules/oop/ComposeEmbedded.tht: -------------------------------------------------------------------------------- 1 | 2 | 3 | class ComposeEmbedded { 4 | 5 | public fields { 6 | embeddedField: 'embedded' 7 | val: 0 8 | } 9 | 10 | public fn embeddedMethod { 11 | return 'embeddedMethod' 12 | } 13 | 14 | public fn conflictMethod { 15 | return 'embedded' 16 | } 17 | 18 | public fn conflictMethodAdded { 19 | return 'embedded' 20 | } 21 | 22 | fn noConflictPrivate { } 23 | 24 | } 25 | 26 | 27 | -------------------------------------------------------------------------------- /tests/code/modules/tests/lib/CookieTests.tht: -------------------------------------------------------------------------------- 1 | 2 | public fun run($t) { 3 | 4 | $t.section('Module: Cookie') 5 | 6 | Cookie.set('test', 'abc123') 7 | $t.ok(Cookie.get('test') == 'abc123', 'Cookie - get/set') 8 | 9 | Cookie.set('deleteMe', 'xxx') 10 | Cookie.delete('deleteMe') 11 | $t.ok(Cookie.get('deleteMe') == '', 'Cookie - delete') 12 | 13 | $t.dies(fun { 14 | Cookie.set('test', '{"a":123}') 15 | }, 'alphanumeric') 16 | 17 | return $t 18 | } 19 | -------------------------------------------------------------------------------- /tests/code/pages/misc/email.tht: -------------------------------------------------------------------------------- 1 | 2 | fun main { 3 | 4 | $status = Email.send({ 5 | fromName: 'Joe Lesko' 6 | from: 'joe@joelesko.com' 7 | to: 'joe+get@joelesko.com' 8 | cc: 'joe+cc@joelesko.com' 9 | subject: 'Test Email' 10 | body: html''' 11 | 12 | Hello {}! 13 | 14 | Timestamp: {} 15 | 16 | '''.fill('Joe', Date.now()) 17 | }) 18 | 19 | print($status) 20 | print(Email.getLastLogs()) 21 | } 22 | 23 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ### THT 2 | 3 | # THT app 4 | **/data/temp/** 5 | **/data/counter/** 6 | **/data/db/** 7 | 8 | *.min.gz 9 | 10 | ### Common 11 | *.bak 12 | *.tmp 13 | 14 | ### Linux 15 | *~ 16 | 17 | # KDE 18 | .directory 19 | 20 | # Vim 21 | .swap 22 | .swp 23 | 24 | ### MacOS 25 | *.DS_Store 26 | .AppleDouble 27 | .LSOverride 28 | Icon 29 | ._* 30 | 31 | ### Windows 32 | Thumbs.db 33 | ehthumbs.db 34 | ehthumbs_vista.db 35 | Desktop.ini 36 | *.lnk 37 | 38 | ### JetBrains IDEs 39 | *.iml 40 | .idea/ 41 | *.ipr 42 | *.iws 43 | -------------------------------------------------------------------------------- /tests/code/pages/errors/-index.tht: -------------------------------------------------------------------------------- 1 | 2 | fun main { 3 | 4 | $files = dir'app:/code/pages/errors'.readDir() 5 | 6 | $page = Page.create({ 7 | title: 'THT Error Pages' 8 | css: url'/css/basic.css' 9 | main: html($files) 10 | }) 11 | 12 | Output.sendPage($page) 13 | } 14 | 15 | 16 | tem html($files) { 17 | 18 |

Error Test Pages 19 | 20 | 25 | } 26 | 27 | 28 | -------------------------------------------------------------------------------- /tests/code/pages/misc/-index.tht: -------------------------------------------------------------------------------- 1 | 2 | fun main { 3 | 4 | $files = dir'app:/code/pages/misc'.readDir() 5 | 6 | $page = Page.create({ 7 | title: 'Manual Test Pages' 8 | css: url'/css/basic.css' 9 | main: html($files) 10 | }) 11 | 12 | Output.sendPage($page) 13 | } 14 | 15 | 16 | tem html($files) { 17 | 18 |

Manual Test Pages 19 | 20 | 25 | 26 | 27 | } 28 | 29 | 30 | -------------------------------------------------------------------------------- /tests/data/logs/contact.txt: -------------------------------------------------------------------------------- 1 | {"date":"2024-05-26 17:35:46","level":"INFO","clientIp":"::1","url":"\/tutorials\/contact-form","name":"dfgdfg","email":"","message":"bcvbcvbcvb","priority":"suggestion","accept":true} 2 | {"date":"2024-05-26 17:36:04","level":"INFO","clientIp":"::1","url":"\/tutorials\/contact-form","name":"dfgdfg","email":"","message":"bcvbcvbcvb","priority":"suggestion","accept":true} 3 | {"date":"2024-05-26 17:36:28","level":"INFO","clientIp":"::1","url":"\/tutorials\/contact-form","name":"dfgdfg","email":"","message":"bcvbcvbcvb","priority":"suggestion","accept":true} 4 | -------------------------------------------------------------------------------- /tests/code/modules/tests/lib/ImageTests.tht: -------------------------------------------------------------------------------- 1 | 2 | public fun run($t) { 3 | 4 | return $t 5 | 6 | $img = Image.create({ sizeX: 300, sizeY: 200 }) 7 | 8 | >> '-- rect --' 9 | $img.drawRectangle( 10 | { x: 1, y: 1, sizeX: 60, center: false } 11 | { color: 'red', fill: true } 12 | ) 13 | 14 | >> '-- ellipse --' 15 | $img.drawEllipse( 16 | { x: 1, y: 1, sizeX: 60, center: false } 17 | { color: 'blue', fill: true } 18 | ) 19 | 20 | // $img.fill({ x: 1, y: 1 }) 21 | 22 | >> $img 23 | 24 | return $t 25 | } 26 | 27 | -------------------------------------------------------------------------------- /tht/lib/core/data/starterApp/code/modules/App.tht: -------------------------------------------------------------------------------- 1 | @@.AppName = 'MyApp' 2 | 3 | public fun newPage { 4 | 5 | $page = Page.create({ 6 | 7 | appName: @@.AppName 8 | icon: url'/images/favicon_128.png' 9 | 10 | header: headerHtml() 11 | footer: footerHtml() 12 | 13 | css: [ 14 | url'/vendor/basic.css' 15 | url'/css/app.css' 16 | ] 17 | }) 18 | 19 | return $page 20 | } 21 | 22 | tem headerHtml { 23 | <.logo> {{ @@.AppName }} 24 | } 25 | 26 | tem footerHtml { 27 | © {{ Date.now().format('Y') }} 28 | } 29 | -------------------------------------------------------------------------------- /tests/code/pages/misc/print.tht: -------------------------------------------------------------------------------- 1 | 2 | 3 | $mixedList = [1, 'b', true, null, Date.now()] 4 | 5 | $mixedMap = { 6 | list: [1, 2, 3] 7 | date: Date.now() 8 | bool: false 9 | nada: null 10 | inner: { z: 999 } 11 | str: 'This is "double" & `back\`tick` & \'escaped\'' 12 | sql: sql'select * from table' 13 | file: file'dir/file.txt' 14 | mixedList: $mixedList 15 | } 16 | 17 | >> $mixedList 18 | >> $mixedMap 19 | 20 | >> '-------------------------------' 21 | 22 | foreach $mixedMap as $k/$v { 23 | print($v) 24 | } 25 | 26 | >> Json.encode($mixedMap).renderString() 27 | 28 | 29 | -------------------------------------------------------------------------------- /tests/code/public/test_perf_issue.php: -------------------------------------------------------------------------------- 1 | getTitle(); 11 | } 12 | 13 | function getTitle() { 14 | return 'THT ' . ucfirst($this->error['category']) . ' Error'; 15 | } 16 | 17 | function getHtml() { 18 | 19 | $out = $this->getTitle(); 20 | $out .= '' . $this->error['origin'] . ''; 21 | 22 | return $out; 23 | } 24 | 25 | } -------------------------------------------------------------------------------- /tht/lib/stdlib/classes/OBoolean.php: -------------------------------------------------------------------------------- 1 | u_to_string(); 11 | } 12 | 13 | function u_z_to_print_string() { 14 | return $this->u_to_string(); 15 | } 16 | 17 | // Casting 18 | 19 | function u_to_number() { 20 | return $this->val ? 1 : 0; 21 | } 22 | 23 | function u_to_boolean() { 24 | return $this->val; 25 | } 26 | 27 | function u_to_string() { 28 | return $this->val ? 'true' : 'false'; 29 | } 30 | } 31 | 32 | -------------------------------------------------------------------------------- /tht/lib/core/data/starterApp/code/pages/examples/hello-query.tht: -------------------------------------------------------------------------------- 1 | 2 | fun main { 3 | 4 | // Get 'name' as a (s)tring from the query. Default to 'World'. 5 | $name = Input.get('name', 's', 'World') 6 | 7 | $html = helloHtml($name) 8 | 9 | Output.sendHtml($html) 10 | } 11 | 12 | tem helloHtml($name) { 13 | 14 |
15 | 16 |

Hello, {{ $name }}! 17 | 18 |

Change "name" in the query string in the address bar. 19 | 20 |

e.g. hello-query?name=Ima+Teapot 21 | 22 | 23 | } 24 | 25 | -------------------------------------------------------------------------------- /tht/lib/core/compiler/Symbol/Symbols/S_PreKeyword.php: -------------------------------------------------------------------------------- 1 | next(); 12 | $this->space('*word '); 13 | 14 | $nextType = $p->symbol->type; 15 | if ($nextType !== 'PRE_KEYWORD' && $nextType !== 'NEW_FUN' && $nextType !== 'NEW_TEMPLATE' && $nextType !== 'CLASS_FIELDS') { 16 | $p->error('Missing `fun` or `tem` keyword'); 17 | } 18 | 19 | $this->addKid($p->parseStatement(0)); 20 | 21 | return $this; 22 | } 23 | } 24 | 25 | -------------------------------------------------------------------------------- /tht/lib/stdlib/classes/OVar.php: -------------------------------------------------------------------------------- 1 | val; 12 | } 13 | 14 | function u_z_to_print_string() { 15 | return $this->val; 16 | } 17 | 18 | public function u_to_string() { 19 | return $this->val; 20 | } 21 | 22 | public function __toString() { 23 | return $this->val; 24 | } 25 | 26 | public function u_z_clone() { 27 | return $this->val; 28 | } 29 | } 30 | 31 | -------------------------------------------------------------------------------- /tht/lib/core/compiler/SourceAnalyzer/SourceMap.php: -------------------------------------------------------------------------------- 1 | map = [ 'file' => $relPath ]; 12 | } 13 | 14 | function set($targetSrcLine) { 15 | $this->map[$this->lineNum] = $targetSrcLine; 16 | } 17 | 18 | function next() { 19 | $this->lineNum += 1; 20 | } 21 | 22 | function out() { 23 | $out = "/* SOURCE=" . json_encode($this->map) . " */\n"; 24 | return $out; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/data/logs/app.log: -------------------------------------------------------------------------------- 1 | {"date":"2024-10-13 17:04:15","level":"TRACE","ip":"::1","url":"/all-tests","message":"trace!"} 2 | {"date":"2024-10-13 17:04:15","level":"DEBUG","ip":"::1","url":"/all-tests","message":"debug!"} 3 | {"date":"2024-10-13 17:04:15","level":"INFO","ip":"::1","url":"/all-tests","message":"info!"} 4 | {"date":"2024-10-13 17:04:15","level":"WARN","ip":"::1","url":"/all-tests","message":"warn!"} 5 | {"date":"2024-10-13 17:04:15","level":"ERROR","ip":"::1","url":"/all-tests","message":"error!"} 6 | {"date":"2024-10-13 17:04:15","level":"FATAL","ip":"::1","url":"/all-tests","message":"fatal!"} 7 | {"date":"2024-10-13 17:04:15","level":"INFO","ip":"::1","url":"/all-tests","a":123,"z":true} 8 | -------------------------------------------------------------------------------- /tht/lib/core/compiler/Symbol/Symbols/S_Command.php: -------------------------------------------------------------------------------- 1 | symbol; 15 | $p->next(); 16 | 17 | if ($p->breakableDepth == 0) { 18 | $p->error('Keyword not allowed outside of a loop: `' . $sCommand->getValue() . '`', $this->token); 19 | } 20 | 21 | if ($sCommand->isValue('break')) { 22 | $p->loopBreaks[count($p->loopBreaks) - 1] = true; 23 | } 24 | 25 | return $this; 26 | } 27 | } 28 | 29 | -------------------------------------------------------------------------------- /tests/code/public/.htaccess: -------------------------------------------------------------------------------- 1 | # THT App 2 | 3 | DirectoryIndex index.html index.php thtApp.php 4 | Options -Indexes 5 | 6 | # Redirect all non-static URLs to THT app 7 | RewriteEngine On 8 | RewriteCond %{REQUEST_FILENAME} !-f 9 | RewriteCond %{REQUEST_FILENAME} !-d 10 | RewriteRule ^(.*)$ /front.php [QSA,NC,L] 11 | 12 | # Uncomment to redirect to HTTPS 13 | # RewriteCond %{HTTPS} off 14 | # RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} 15 | 16 | # Compression 17 | 18 | 19 | # AddOutputFilterByType DEFLATE \ 20 | # "application/javascript" \ 21 | # "application/json" \ 22 | # "text/css" \ 23 | # "text/html" \ 24 | # "text/javascript" \ 25 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /tests/code/modules/tests/lang/BitwiseTests.tht: -------------------------------------------------------------------------------- 1 | 2 | public fun run($t) { 3 | 4 | $t.section('Bitwise Operators') 5 | 6 | .ok(1 +| 2 == 3, 'bitwise or (+|)') 7 | .ok(2 +& 3 == 2, 'bitwise AND (+&)') 8 | .ok(1 +^ 2 == 3, 'bitwise XOR (+^)') 9 | 10 | .ok(+~5 == -6, 'bitwise NOT (+~)') 11 | 12 | .ok(3 +< 2 == 12, 'bitwise shift LEFT (+<)') 13 | .ok(13 +> 2 == 3, 'bitwise shift RIGHT (+>)') 14 | 15 | .ok(0b100 +| 0b010 == 0b110, 'OR (+|) with binary number') 16 | .ok(0b100 +& 0b110 == 0b100, 'AND (+&) with binary number') 17 | .ok(0b100 +^ 0b110 == 0b010, 'XOR (+^) with binary number') 18 | .ok(+~0b110 == -7, 'NOT (+~) with binary number') 19 | 20 | return $t 21 | } 22 | 23 | -------------------------------------------------------------------------------- /tht/lib/core/main/Error/ErrorPage/components/ErrorPageMessage.php: -------------------------------------------------------------------------------- 1 | error['message']; 12 | 13 | $m = ErrorTextUtils::formatMessage($m); 14 | 15 | 16 | return $m; 17 | } 18 | 19 | function getHtml() { 20 | 21 | $out = $this->get(); 22 | 23 | $out = Security::escapeHtml($out); 24 | 25 | $out = str_replace("\n", '
', $out); 26 | 27 | // Convert backticks to code 28 | $out = preg_replace("/`(.*?)`/", '$1', $out); 29 | 30 | return $out; 31 | } 32 | 33 | } -------------------------------------------------------------------------------- /tht/lib/core/compiler/Symbol/Symbols/S_ClassFields.php: -------------------------------------------------------------------------------- 1 | inClass) { 13 | $p->error('Keyword `fields` should only appear within a class.'); 14 | } 15 | 16 | $p->space('*fields '); 17 | $p->next(); 18 | $p->now('{'); 19 | 20 | $sMap = $p->parseExpression(0); 21 | 22 | if ($sMap->value !== AstList::MAP) { 23 | Tht::error('Keyword `fields` must be followed by a Map.'); 24 | } 25 | 26 | $this->addKid($sMap); 27 | 28 | return $this; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tht/lib/core/compiler/Symbol/Symbols/S_Loop.php: -------------------------------------------------------------------------------- 1 | space('*loopS'); 15 | 16 | $sFor = $p->symbol; 17 | $p->next(); 18 | 19 | $p->loopBreaks []= false; 20 | 21 | $p->breakableDepth += 1; 22 | $this->addKid($p->parseBlock()); 23 | $p->breakableDepth -= 1; 24 | 25 | $hasBreak = array_pop($p->loopBreaks); 26 | if (!$hasBreak) { 27 | $p->error("`loop` needs a `break` or `return` statement.", $sFor->token); 28 | } 29 | return $this; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tht/lib/core/compiler/Symbol/Symbols/S_TemplateExpr.php: -------------------------------------------------------------------------------- 1 | space('{{ '); 12 | 13 | $p->next(); 14 | 15 | $this->addKid($p->symbol); // context string 16 | 17 | $p->next(); 18 | $p->next(); // skip ',' 19 | 20 | $this->addKid($p->symbol); // indent level 21 | 22 | $p->next(); 23 | $p->next(); // skip ',' 24 | 25 | $this->addKid($p->parseExpression(0)); 26 | 27 | $p->space(' }}*'); 28 | $p->now(Glyph::TEMPLATE_EXPR_END, 'template.expr.end')->next(); 29 | 30 | return $this; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /tht/lib/core/compiler/Symbol/Symbols/S_ShortPrint.php: -------------------------------------------------------------------------------- 1 | > 'some message' 8 | function asStatement($p) { 9 | 10 | if (!$this->hasNewlineBefore()) { 11 | ErrorHandler::setHelpLink('/language-tour/intermediate/shortcuts#print', 'Print Shortcut'); 12 | $p->error('Print shortcut `>>` can only be used at the beginning of a line. Try: `+>` (bit-shift right)'); 13 | } 14 | 15 | $p->next(); 16 | 17 | $this->space('B>> '); 18 | 19 | $p->expressionDepth += 1; // prevent assignment 20 | $sReturnVal = $p->parseExpression(0, 'noOuterParens'); 21 | $this->addKid($sReturnVal); 22 | 23 | return $this; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /tht/lib/core/compiler/_index.php: -------------------------------------------------------------------------------- 1 | error['source']['file'], true 11 | ); 12 | } 13 | 14 | function getHtml() { 15 | 16 | $out = $this->get(); 17 | 18 | if (!$out) { return ''; } 19 | 20 | $out = preg_replace('#(.*/)(.*)#', '$1$2', $out); 21 | $out = preg_replace('#(.*)(\.\w{2,4})$#', '$1$2', $out); 22 | $out = $this->wrapHtml('span', '', $out); 23 | 24 | $out = 'File:' . $out; 25 | 26 | return $out; 27 | } 28 | 29 | } -------------------------------------------------------------------------------- /tests/code/modules/oop/ComposeParent.tht: -------------------------------------------------------------------------------- 1 | 2 | load('oop/ComposeHelper') 3 | load('oop/ComposeEmbedded') 4 | load('oop/ComposeEmbeddedOther') 5 | load('oop/ComposeEmbeddedConflict') 6 | 7 | class ComposeParent { 8 | 9 | attach { 10 | embed: ComposeEmbedded({ val: 123 }) 11 | other: ComposeEmbeddedOther() 12 | } 13 | 14 | public fields { 15 | helper: ComposeHelper() 16 | } 17 | 18 | public fn useHelper { 19 | return 'helper:' ~ @.helper.getNum() 20 | } 21 | 22 | public fn useEmbedded { 23 | return 'embedded:' ~ @.ComposeEmbedded.val 24 | } 25 | 26 | public fn conflictMethod { 27 | return @.ComposeEmbeddedOther 28 | } 29 | 30 | public fn addConflict { 31 | @.zAddEmbeddedObject(ComposeEmbeddedConflict()) 32 | } 33 | 34 | } 35 | 36 | -------------------------------------------------------------------------------- /tht/lib/core/main/Error/ErrorPage/components/ErrorPageComponent.php: -------------------------------------------------------------------------------- 1 | errorPage = $errorPage; 13 | $this->error = $errorPage->error; 14 | } 15 | 16 | function get() { 17 | return ''; 18 | } 19 | 20 | function getHtml() { 21 | $this->isHtml = true; 22 | return $this->get(); 23 | } 24 | 25 | function wrapHtml($el, $className, $out) { 26 | 27 | return "<$el class=\"$className\">$out"; 28 | } 29 | 30 | function out($htmlOut, $plainTextOut) { 31 | return $this->isHtml ? $htmlOut : $plainTextOut; 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /tests/code/modules/tests/lang/TypeHints.tht: -------------------------------------------------------------------------------- 1 | // Undocumented. 2 | // Taking arguments by class name requires a re-factoring of internal namespacing. 3 | 4 | public fun run($t) { 5 | 6 | $t.section('Type Hints') 7 | 8 | $t.ok(takeString('x') == 'takeString:x', 'take string') 9 | $t.ok(takeString() == 'takeString:default', 'take string - default') 10 | 11 | return $t 12 | } 13 | 14 | fun takeString($arg:s = 'default') { 15 | return 'takeString:' ~ $arg 16 | } 17 | 18 | 19 | // Return types will probably use arrow. (like Python) 20 | 21 | // fun takeString($arg:s = 'default') -> s { 22 | // return 'takeString:' ~ $arg 23 | // } 24 | 25 | // This also matches the nomenclature used in the tht.dev docs. 26 | // It also means we need to be a bit smarter about catching use of PHP-style -> instead of dot (.). 27 | 28 | 29 | -------------------------------------------------------------------------------- /tests/code/modules/tests/lib/AppConfigTests.tht: -------------------------------------------------------------------------------- 1 | 2 | public fun run($t) { 3 | 4 | $t.section('Module: AppConfig') 5 | 6 | $t.ok(AppConfig.get('num') == -123.45, 'get num') 7 | $t.ok(AppConfig.get('booleanFalse') == false, 'get boolean') 8 | $t.ok(AppConfig.get('booleanTrue') == true, 'get boolean') 9 | $t.ok(AppConfig.get('string') == 'value with spaces, etc.', 'get string') 10 | $t.ok(AppConfig.get('map').key == 'value', 'get map') 11 | $t.ok(AppConfig.get('list')[2] == 'value 2', 'get list') 12 | $t.dies( 13 | fun { AppConfig.get('MISSING') }, 'missing key' 14 | 'No `app` config value for key' 15 | ) 16 | $t.ok(AppConfig.get('local').localVar == 789, 'local config') 17 | 18 | $t.ok(AppConfig.get('aUrl').rawString() == 'https://asite.com', 'url typestring') 19 | 20 | return $t 21 | } 22 | -------------------------------------------------------------------------------- /tests/code/pages/benchmark/test-route.tht: -------------------------------------------------------------------------------- 1 | 2 | fun main { 3 | 4 | $pid = Input.route('pid') 5 | 6 | $items = [] 7 | foreach range(1, 100) as $i { 8 | 9 | $item = { 10 | num: $i 11 | name: $pid ~ $i 12 | } 13 | 14 | $items #= $item 15 | } 16 | 17 | Output.sendHtml(html($pid, $items)) 18 | } 19 | 20 | tem html($pid, $items) { 21 | 22 | 23 | 24 | Route Param Test 25 | </> 26 | <body> 27 | THT - Route Param: {{ $pid }} 28 | <ul> 29 | --- foreach $items as $item { 30 | {{ itemHtml($item) }} 31 | --- } 32 | </> 33 | </> 34 | </> 35 | } 36 | 37 | tem itemHtml($item) { 38 | <li> {{ $item.num }}: {{ $item.name }} 39 | } 40 | 41 | -------------------------------------------------------------------------------- /tht/lib/core/compiler/templateTransformers/CssTemplateTransformer.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | class CssTemplateTransformer extends TemplateTransformer { 6 | 7 | function onEndString($str) { 8 | 9 | if (Tht::getThtConfig('minifyAssetTemplates')) { 10 | 11 | $hasTrailingSpace = preg_match('/ $/', $str); 12 | $hasLeadingSpace = preg_match('/^ /', $str); 13 | 14 | $str = Tht::module('Output')->minifyCss($str); 15 | 16 | // Handle case like 'border: solid {{ $width }}' 17 | // to prevent value from connecting with prev string 18 | if ($hasTrailingSpace) { 19 | $str .= ' '; 20 | } 21 | if ($hasLeadingSpace) { 22 | $str = ' ' . $str; 23 | } 24 | } 25 | 26 | return $str; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /tht/lib/core/compiler/Symbol/Symbols/S_Return.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | class S_Return extends S_Command { 6 | 7 | var $allowAsMapKey = true; 8 | 9 | // e.g. return 1; 10 | function asStatement($p) { 11 | 12 | if ($p->functionDepth == 0) { 13 | $p->error('`return` not allowed outside of a function.'); 14 | } 15 | 16 | $p->next(); 17 | if (!$p->symbol->isNewline()) { 18 | $this->space('*return '); 19 | $p->expressionDepth += 1; // prevent assignment 20 | $sReturnVal = $p->parseExpression(0, 'noOuterParens'); 21 | $this->addKid($sReturnVal); 22 | } 23 | 24 | $p->loopBreaks[count($p->loopBreaks) - 1] = true; 25 | 26 | // Don't check for orphan, to support a common debugging pattern of returning early 27 | 28 | return $this; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /tht/lib/core/data/starterApp/code/pages/examples/hello-page.tht: -------------------------------------------------------------------------------- 1 | 2 | fun main { 3 | 4 | $page = Page.create({ 5 | title: 'My Page Title' 6 | description: 'This is an example page.' 7 | css: [url'vendor/basic.css'] 8 | 9 | header: headerHtml() 10 | footer: footerHtml() 11 | main: pageHtml() 12 | }) 13 | 14 | Output.sendPage($page) 15 | } 16 | 17 | tem pageHtml { 18 | 19 | <h1> Hello World 20 | 21 | <p> This is a full HTML document created with the Page module. 22 | 23 | <p> Use "View Page Source" in your browser to see the final HTML output. 24 | } 25 | 26 | tem headerHtml { 27 | <div style="background-color: #ddd; padding: 1rem 2rem"> 28 | Header 29 | </> 30 | } 31 | 32 | tem footerHtml { 33 | <div style="border-top: solid 2px #ccc; padding: 2rem; text-align: center"> 34 | Footer 35 | </> 36 | } -------------------------------------------------------------------------------- /tht/lib/core/main/Error/ErrorPage/components/ErrorPageObjectDetail.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | require_once('ErrorPageComponent.php'); 6 | 7 | class ErrorPageObjectDetail extends ErrorPageComponent { 8 | 9 | // TODO: fix/implement this 10 | function get() { 11 | 12 | // $obj = $this->error['objectDetail']; 13 | 14 | // if ($obj) { 15 | 16 | // $formatted = Tht::module('Bare')->formatObjectDetail($obj); 17 | // $className = $this->error['objectDetailName'] ?: $obj->bareClassName(); 18 | // $firstLine = $this->out("<div class=\"tht-error-trace-heading\">Object Detail: $className</div>", "Object Detail:\n\n"); 19 | // $formatted = $this->out('<div class="tht-color-code theme-dark">' . $formatted . '</div>', $formatted); 20 | 21 | // return $firstLine . $formatted; 22 | // } 23 | 24 | return ''; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /tests/code/pages/user-profile.tht: -------------------------------------------------------------------------------- 1 | 2 | // Used for creating screenshots of the THT error page with stack trace. 3 | 4 | 5 | 6 | 7 | fun main { 8 | 9 | $allUsers = getFollowers(3683, -filter) 10 | 11 | } 12 | 13 | 14 | 15 | fun getFollowers($userId, $flags) { 16 | 17 | $users = [ 18 | { id: 1, name: 'tacotime33', isActive: false } 19 | { id: 2, name: 'puppydog1', isActive: false } 20 | { id: 3, name: 'sidchip64', isActive: true } 21 | ] 22 | 23 | $users = getUserNames($users) 24 | 25 | return $users 26 | } 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | fun getUserNames($allUsers) { 50 | 51 | $userNames = [] 52 | foreach $allUsers as $user { 53 | if $user.isActive { 54 | $userNames #= $user.name.toUpper() 55 | } 56 | } 57 | 58 | return $userNames 59 | } 60 | 61 | 62 | 63 | -------------------------------------------------------------------------------- /tests/code/modules/tests/lib/LogTests.tht: -------------------------------------------------------------------------------- 1 | 2 | 3 | // TODO: test logLevel limit set in app.jcon 4 | public fun run($t) { 5 | 6 | if $t.skipSlowTests(): return $t 7 | 8 | $t.section('Log') 9 | 10 | $logFile = Log.getFile() 11 | $logFile.delete(-ifExists) 12 | 13 | Log.trace('trace!') 14 | Log.debug('debug!') 15 | Log.info('info!') 16 | Log.warn('warn!') 17 | Log.error('error!') 18 | Log.fatal('fatal!') 19 | 20 | Log.info({ a: 123, z: true }) 21 | 22 | $lines = $logFile.read() 23 | 24 | $levels = q[ 25 | trace 26 | debug 27 | info 28 | warn 29 | error 30 | fatal 31 | ] 32 | 33 | foreach $levels as $i/$level { 34 | $rx = Regex('"{}".*{}!'.fill([$level.toUpperCase(), $level])) 35 | $t.ok($lines[$i].match($rx), $level) 36 | } 37 | 38 | $lastLine = $lines.pop() 39 | $t.ok($lastLine.contains('"a":123,"z":true'), 'json event') 40 | 41 | return $t 42 | } 43 | -------------------------------------------------------------------------------- /tht/lib/core/compiler/Tokenizer/TokenStream.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | class TokenStream { 6 | 7 | var $tokens = []; 8 | 9 | function add($t) { 10 | $this->tokens []= implode(TOKEN_SEP, $t); 11 | } 12 | 13 | function done() { 14 | if (count($this->tokens) <= 1) { 15 | // add a noop token to prevent error if there are no tokens (e.g. all comments) 16 | $this->add([TokenType::WORD, '1,1', 0, 'false']); 17 | } 18 | // array_pop is much faster than array_shift, so reverse it 19 | $this->tokens = array_reverse($this->tokens); 20 | } 21 | 22 | function count() { 23 | return count($this->tokens); 24 | } 25 | 26 | function next() { 27 | $t = array_pop($this->tokens); 28 | return explode(TOKEN_SEP, $t, 4); 29 | } 30 | 31 | function lookahead() { 32 | $t = $this->tokens[count($this->tokens) - 1]; 33 | return explode(TOKEN_SEP, $t, 4); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/code/modules/tests/lib/PasswordTests.tht: -------------------------------------------------------------------------------- 1 | public fun run($t) { 2 | 3 | $t.section('Module: Password') 4 | 5 | if $t.skipSlowTests(): return $t 6 | 7 | $p1 = String.xDangerPassword('p@ssw0rd ⒶⒷⒸ 123') 8 | $h1 = $p1.xDangerHash() 9 | 10 | $t.ok($h1.left(3) == '$2y' && $h1.length() >= 60, 'hash') 11 | 12 | $t.ok($p1.check($h1), 'check') 13 | $t.ok(!$p1.check('nope'), 'check - false') 14 | 15 | $t.ok($p1.checkPattern(rx'\d{3}'), 'checkPattern') 16 | $t.ok(!$p1.checkPattern(rx'[xyz]+'), 'checkPattern - false') 17 | 18 | $t.ok($p1.length() == 16, 'length') 19 | 20 | $t.dies( 21 | fun { String.xDangerPassword($h1).xDangerHash() } 22 | 'can not be hashed a 2nd time' 23 | ) 24 | 25 | $map = { 26 | pass: String.xDangerPassword('123') 27 | } 28 | $json = Json.encode($map).renderString() 29 | $t.ok(!$json.contains('123'), 'Json encode = no plaintext') 30 | 31 | // TODO: test insert to db 32 | 33 | return $t 34 | } -------------------------------------------------------------------------------- /tests/code/modules/tests/lib/RequestTests.tht: -------------------------------------------------------------------------------- 1 | 2 | // TODO: This is all hardcoded to my local setup 3 | public fun run($t) { 4 | 5 | $t.section('Module: Request') 6 | 7 | if $t.skipSlowTests(): return $t 8 | 9 | $t.ok(Request.getIp(), 'ip') 10 | $t.ok(Request.getIp(-all).zClassName() == 'List', 'ip list') 11 | 12 | $ua = Request.getUserAgent() 13 | $t.ok($ua.os == 'mac' || $ua.os == 'windows', 'userAgent - os') 14 | $t.ok($ua.browser == 'chrome' || $ua.browser == 'firefox', 'userAgent - browser') 15 | $t.ok($ua.full.contains('Mozilla'), 'userAgent - full') 16 | $t.ok(Request.getLanguages()[1] == 'en-us', 'languages') 17 | 18 | $t.ok(Request.isHttps() == false, 'isHttps') 19 | $t.ok(Request.isAjax() == false, 'isAjax') 20 | $ref = Request.getReferrer() 21 | $t.ok($ref == '' || $ref.contains('localhost', -ignoreCase), 'referrer') 22 | 23 | $t.ok(Request.getMethod() == 'get', 'method') 24 | $t.ok(Request.getHeaders().hasKey('accept-encoding'), 'headers') 25 | 26 | return $t 27 | } 28 | 29 | -------------------------------------------------------------------------------- /tests/code/modules/tests/lib/MathTests.tht: -------------------------------------------------------------------------------- 1 | 2 | public fun run($t) { 3 | 4 | $t.section('Module: Math') 5 | 6 | $rand = Math.random(6, 8) 7 | $rnd = Math.random() 8 | 9 | $t 10 | .ok($rand >= 6 && $rand <= 8, 'random') 11 | .ok($rand.floor() == $rand, 'random is int') 12 | .ok($rnd >= 0.0 && $rnd < 1.0, 'random float') 13 | 14 | .ok(Math.convertBase(21, 10, 2) == '10101', 'convertBase: dec to bin') 15 | .ok(Math.convertBase('1af9', 16, 10) == 6905, 'convertBase: hex to dec') 16 | 17 | .ok(Math.hexToDec('F1') == 241, 'hexToDex') 18 | .ok(Math.hexToDec('FF0000') == 16711680, 'hexToDex - color') 19 | .ok(Math.hexToDec('#FF0000') == 16711680, 'hexToDex - CSS color') 20 | .ok(Math.hexToDec('0xFF0000') == 16711680, 'hexToDex - 0x prefix') 21 | .ok(Math.decToHex(241) == 'f1', 'decToHex') 22 | .ok(Math.decToHex(16) == '10', 'decToHex') 23 | 24 | .ok(Math.sqrt(16) == 4, 'sqrt') 25 | .ok(Math.pi().round(2) == 3.14, 'pi') 26 | 27 | return $t 28 | } 29 | -------------------------------------------------------------------------------- /tests/code/modules/tests/lib/SystemTests.tht: -------------------------------------------------------------------------------- 1 | 2 | public fun run($t) { 3 | 4 | $t.section('Module: System') 5 | 6 | $t.ok(System.getEnvVar('PATH') != '', 'envVar - PATH') 7 | $t.ok(System.getEnvVar('DFSDFHJSF') == '', 'envVar - missing') 8 | $t.ok(System.getEnvVar('DFSDFHJSF', 'def') == 'def', 'envVar - default') 9 | $t.ok(System.getStartTime() > 10000000, 'startTime') 10 | $t.ok(System.getMemoryUsage() > 0, 'memoryUsage') 11 | $t.ok(System.getPeakMemoryUsage() > 0, 'peakMemoryUsage') 12 | $t.ok(System.getOs().match(rx'(mac|windows|linux)'), 'os') 13 | 14 | $t.dies(fun { System.setMaxRunTimeSecs(-1) }, 'positive integer') 15 | $t.dies(fun { System.setMaxRunTimeSecs(0) }, '`maxRunTimeSecs` must be greater than 0') 16 | $t.dies(fun { System.setMaxMemoryMb(-1) }, 'positive integer') 17 | $t.dies(fun { System.setMaxMemoryMb(0) }, '`maxMemoryMb` must be greater than 0') 18 | 19 | // Can't be called in webMode 20 | //$t.ok(System.command(cmd'ls').output[0] == 'thtApp.php', 'command') 21 | 22 | return $t 23 | } 24 | -------------------------------------------------------------------------------- /tht/lib/stdlib/classes/ONull.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | class ONull extends OVar { 6 | 7 | protected $type = 'null'; 8 | 9 | function __toString() { 10 | return 'null'; 11 | } 12 | 13 | public function jsonSerialize():mixed { 14 | return null; 15 | } 16 | 17 | function u_z_to_print_string() { 18 | return 'null'; 19 | } 20 | 21 | // Casting 22 | 23 | function u_to_number() { 24 | return 0; 25 | } 26 | 27 | function u_to_string() { 28 | return 'null'; 29 | } 30 | 31 | function u_to_boolean() { 32 | return false; 33 | } 34 | 35 | function __call($fnName, $args) { 36 | return $this->error("Can't call method on null object: `$fnName`"); 37 | } 38 | 39 | function __get($fieldName) { 40 | return $this->error("Can't get field on null object: `$fieldName`"); 41 | } 42 | 43 | function __set($fieldName, $val) { 44 | return $this->error("Can't set field on null object: `$fieldName`"); 45 | } 46 | } 47 | 48 | 49 | -------------------------------------------------------------------------------- /tht/lib/core/data/starterApp/code/pages/home.tht: -------------------------------------------------------------------------------- 1 | fun main { 2 | 3 | // Call `newPage` function in 'code/modules/App.tht' 4 | $page = App.newPage() 5 | 6 | // Set page-specific fields 7 | $page.setTitle('Home') 8 | $page.setMain(pageHtml()) 9 | 10 | Output.sendPage($page) 11 | } 12 | 13 | fun getExamplePages { 14 | return [ 15 | 'hello' 16 | 'hello-html' 17 | 'hello-page' 18 | 'hello-query' 19 | 'hello-form' 20 | 'route-colors' 21 | 'contact-form' 22 | 'checkbox-form' 23 | 'upload-form' 24 | 'ajax-weather' 25 | 'database' 26 | ] 27 | } 28 | 29 | tem pageHtml { 30 | 31 | <h1> {{ Web.icon('check') }} Ready 32 | 33 | <.subline> This app is ready for development. 34 | 35 | <h2>Edit this page at: 36 | <p> code/pages/<b>home.tht</> 37 | 38 | <h2> code/pages/examples 39 | 40 | <ul> 41 | --- foreach getExamplePages() as $page { 42 | <li> <a href="/examples/{{ $page }}"> {{ $page }}.tht 43 | --- } 44 | </> 45 | } 46 | -------------------------------------------------------------------------------- /tests/code/modules/oop/BaseObject.tht: -------------------------------------------------------------------------------- 1 | 2 | @@.BaseModuleConstant = 'constant' 3 | 4 | 5 | class BaseObject { 6 | 7 | fields { 8 | privateVar: 'private' 9 | privateNoInit: 'noInit' 10 | } 11 | 12 | public fields { 13 | publicVar: 'public' 14 | overrideNew: 222 15 | overrideInit: 111 16 | getterVal: 'getter' 17 | 18 | num: 123 19 | flag: true 20 | string: 'xyz' 21 | map: { a: 123 } 22 | list: [1, 2, 3] 23 | } 24 | 25 | public fun onCreate { 26 | @.overrideNew = 444 27 | } 28 | 29 | public fun getGetterVal { 30 | return @.getterVal ~ '!' 31 | } 32 | 33 | public fun publicCallPrivate { 34 | return @.privateFn() 35 | } 36 | 37 | public fun readNoInit { 38 | return @.privateNoInit 39 | } 40 | 41 | public fun publicFn { 42 | return 'public' 43 | } 44 | 45 | public fun returnSelf { 46 | return @ 47 | } 48 | 49 | fun privateFn { 50 | return 'private' 51 | } 52 | } 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /tht/lib/core/data/starterApp/code/pages/examples/route-colors.tht: -------------------------------------------------------------------------------- 1 | fun main { 2 | 3 | // Get {color} route param, default to 'red' 4 | $color = Input.route('color', 'id', 'red') 5 | 6 | $page = Page.create({ 7 | title: 'Colors' 8 | css: [url'vendor/basic.css'] 9 | }) 10 | 11 | $html = pageHtml($color) 12 | $page.setMain($html) 13 | 14 | Output.sendPage($page) 15 | } 16 | 17 | tem pageHtml($color) { 18 | 19 | <div style="border-top: solid 2rem {{ $color }}; padding-top: 1rem"> 20 | 21 | <h1> {{ $color.toTitleCase() }} 22 | 23 | {{ linksHtml() }} 24 | 25 | </> 26 | } 27 | 28 | tem linksHtml { 29 | 30 | --- $colors = ['red', 'aqua', 'orange'] 31 | 32 | --- foreach $colors as $color { 33 | 34 | <a.button href="/examples/route-colors/{{ $color }}"> 35 | {{ $color.toTitleCase() }} 36 | </> 37 | 38 | --- } 39 | 40 | <.panel style="margin-top: 8rem"> 41 | <p> This page uses a route defined in <code>config/app.jcon</code> as: 42 | <pre> /examples/route-colors/{color}: examples/route-colors.tht 43 | </> 44 | } 45 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Joseph Lesko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /tht/lib/core/compiler/TemplateTransformers/TemplateTransformers.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | TemplateTransformer::loadTransformers(); 6 | 7 | class TemplateTransformer { 8 | 9 | protected $tokenizer = null; 10 | protected $currentContext = 'none'; 11 | protected $currentIndent = 0; 12 | protected $reader = null; 13 | 14 | static function loadTransformers() { 15 | 16 | $transformers = [ 17 | 'Html', 18 | 'Css', 19 | 'Lm', 20 | 'Js', 21 | 'Text', 22 | 'Jcon', 23 | ]; 24 | 25 | foreach ($transformers as $t) { 26 | require_once($t . 'TemplateTransformer.php'); 27 | } 28 | } 29 | 30 | function __construct($reader) { 31 | $this->reader = $reader; 32 | } 33 | 34 | function transformNext() { 35 | return false; 36 | } 37 | 38 | function onEndChunk($s) { 39 | return $s; 40 | } 41 | 42 | function onEndBody() {} 43 | 44 | function onEndFile() {} 45 | 46 | function currentContext() { 47 | return $this->currentContext; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tht/lib/stdlib/classes/_index.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | class StdLibClasses { 6 | 7 | static public $files = [ 8 | 'OClass', 9 | 'OStdModule', 10 | 'OVar', 11 | 'OBag', 12 | 'OList', 13 | 'OMap', 14 | 'ONumber', 15 | 'ORegex', 16 | 'OString', 17 | 'OBoolean', 18 | 'ONull', 19 | 'OFunction', 20 | 'OTypeString', 21 | 'OModule', 22 | 'OTemplate', 23 | 'OUrlQuery', 24 | 'OPassword', 25 | ]; 26 | 27 | static public $typeStrings = [ 28 | 'OCore', 29 | 'OUrl', 30 | 'OPath', 31 | 'OPath/OFile', 32 | 'OPath/ODir', 33 | ]; 34 | 35 | static public function load() { 36 | 37 | foreach (self::$files as $lib) { 38 | require_once($lib . '.php'); 39 | } 40 | 41 | foreach (self::$typeStrings as $ts) { 42 | require_once('OTypeString/' . $ts . '.php'); 43 | } 44 | } 45 | 46 | public static function isa($cls) { 47 | return in_array($cls, self::$files); 48 | } 49 | } 50 | 51 | StdLibClasses::load(); 52 | -------------------------------------------------------------------------------- /tests/code/pages/misc/form-login.tht: -------------------------------------------------------------------------------- 1 | 2 | @@.form = Form.create('loginForm', { 3 | username: { 4 | rule: 'username' 5 | } 6 | password: { 7 | rule: 'password' 8 | } 9 | }) 10 | 11 | fun main { 12 | 13 | $page = Page.create({ 14 | title: 'Login Form' 15 | main: formHtml() 16 | css: [url'/css/basic.css'] 17 | js: [url'/js/form.js'] 18 | }) 19 | 20 | Output.sendPage($page) 21 | } 22 | 23 | fun postMode { 24 | 25 | @@.form.process(fun ($data) { 26 | 27 | if $data.password.check('$2y$10$wFKvNdyD4NmRJEV5tFdWT.AFH6kd1Qc/oKJIC6v8dMz5q6N8tGune') { 28 | return thanksHtml($data) 29 | } 30 | else { 31 | return ['password', 'Incorrect password.'] 32 | } 33 | 34 | }) 35 | } 36 | 37 | tem formHtml { 38 | 39 | <h1> Login Form 40 | 41 | <.panel> 42 | {{ @@.form.toHtml('Log In') }} 43 | </> 44 | 45 | } 46 | 47 | tem thanksHtml($data) { 48 | 49 | <p> Welcome back, <b>{{ $data.username }}</>! 50 | 51 | <p> Hashed password: <b> {{ $data.password.xDangerHash() }} 52 | 53 | <p> <a href=""> Back to Form 54 | } 55 | -------------------------------------------------------------------------------- /tests/code/pages/misc/form-upload.tht: -------------------------------------------------------------------------------- 1 | 2 | @@.form = Form.create('uploadForm', { 3 | 4 | name: { 5 | rule: 'name' 6 | } 7 | uploadFile: { 8 | rule: { 9 | type: 'file' 10 | maxSizeMb: 5 11 | ext: 'pdf' 12 | dir: dir'app:/data/uploads' 13 | } 14 | } 15 | }) 16 | 17 | fun main { 18 | 19 | $page = Page.create({ 20 | title: 'Upload Form' 21 | main: formHtml() 22 | css: [url'/css/basic.css'] 23 | js: [url'/js/form.js'] 24 | }) 25 | 26 | Output.sendPage($page) 27 | } 28 | 29 | fun postMode { 30 | 31 | @@.form.process(fun ($data) { 32 | 33 | return thanksHtml($data) 34 | }) 35 | } 36 | 37 | tem formHtml { 38 | 39 | <h1> Upload Form 40 | 41 | <.panel> 42 | --- $buttonLabel = Web.icon('check').append(html' Upload File') 43 | {{ @@.form.toHtml($buttonLabel) }} 44 | </> 45 | 46 | } 47 | 48 | tem thanksHtml($data) { 49 | 50 | <p> Thanks <b>{{ $data.name }}</>, we got your upload! 51 | 52 | <p> File: {{ $data.uploadFile.renderString() }} 53 | 54 | <p> <a href=""> Back to Form 55 | } 56 | -------------------------------------------------------------------------------- /tests/code/php/vendor/testVendorClass.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace Abc; 4 | 5 | class VendorClass { 6 | 7 | var $ALL_CAP_FIELD = 123; 8 | 9 | public static function staticFunction($arg) { 10 | return "STATIC: " . $arg; 11 | } 12 | 13 | function takeArray ($ary) { 14 | return array_shift($ary); 15 | } 16 | 17 | function returnArray () { 18 | return ['a', 'b', 'c']; 19 | } 20 | 21 | function takeMap ($ary) { 22 | return $ary['red']; 23 | } 24 | 25 | function returnMap () { 26 | return [ 'id' => 123, 'color' => 'Red' ]; 27 | } 28 | 29 | function returnObject() { 30 | return new VendorSubClass (); 31 | } 32 | 33 | function returnRecords () { 34 | 35 | return [ 36 | [ 'id' => 123, 'color' => 'Red' ], 37 | [ 'id' => 124, 'color' => 'Green' ], 38 | [ 'id' => 125, 'color' => 'Blue' ], 39 | ]; 40 | } 41 | 42 | function ALL_CAP_METHOD() { 43 | return 'FOO'; 44 | } 45 | 46 | } 47 | 48 | class VendorSubClass { 49 | 50 | function callMe() { 51 | return 'abc'; 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /tht/lib/core/data/starterApp/code/public/.htaccess: -------------------------------------------------------------------------------- 1 | ### THT APP 2 | 3 | DirectoryIndex index.html index.php front.php 4 | Options -Indexes 5 | 6 | # Redirect all non-static URLs to THT app 7 | RewriteEngine On 8 | RewriteCond %{REQUEST_FILENAME} !-f 9 | RewriteCond %{REQUEST_FILENAME} !-d 10 | RewriteRule ^(.*)$ /front.php?_url=$1 [QSA,NC,L] 11 | 12 | # Uncomment to redirect to HTTPS 13 | # RewriteCond %{HTTPS} off 14 | # RewriteRule (.*) https://%{HTTP_HOST}%{REQUEST_URI} 15 | 16 | # --- Performance --- 17 | 18 | # Activate client-side caching (30 days) of assets with 'v' param 19 | # See https://tht.dev/manual/module/web/asset-url 20 | <If "%{QUERY_STRING} =~ /v=\d+/ && %{REQUEST_URI} =~ /\.\w+$/"> 21 | Header set Cache-Control "public, max-age=2592000, immutable" 22 | Header unset ETag 23 | FileETag None 24 | </If> 25 | 26 | # Required for pre-gzipped assets 27 | <FilesMatch \.gz$> 28 | Header set Content-Encoding gzip 29 | RewriteRule \.css.*?\.gz$ - [T=text/css,E=no-gzip:1] 30 | RewriteRule \.js.*?\.gz$ - [T=text/javascript,E=no-gzip:1] 31 | 32 | AddOutputFilterByType DEFLATE image/x-icon 33 | </FilesMatch> 34 | 35 | ### END THT APP 36 | -------------------------------------------------------------------------------- /tht/lib/core/compiler/Symbol/Symbols/S_TryCatch.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | class S_TryCatch extends S_Statement { 6 | 7 | var $type = SymbolType::TRY_CATCH; 8 | 9 | var $allowAsMapKey = true; 10 | 11 | // try { ... } catch (e) { ... } 12 | function asStatement($p) { 13 | 14 | $p->space('*tryS'); 15 | 16 | $p->next(); 17 | 18 | // try 19 | $this->addKid($p->parseBlock()); 20 | 21 | // catch 22 | 23 | $p->validator->newScope(); 24 | $p->now('catch', 'try.catch')->space(' catchS')->next(); 25 | 26 | $errorVar = $p->parseExpression(0, 'noOuterParen'); 27 | $p->validator->defineVar($errorVar, true); 28 | $this->addKid($errorVar); 29 | 30 | $this->addKid($p->parseBlock(true)); 31 | 32 | $p->validator->popScope(); // block 33 | $p->validator->popScope(); // catch 34 | $p->next(); 35 | 36 | // finally 37 | if ($p->symbol->isValue('finally')) { 38 | $p->space(' finally '); 39 | $p->next(); 40 | $this->addKid($p->parseBlock()); 41 | } 42 | 43 | return $this; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tht/lib/core/main/Error/ErrorHandlerMinimal.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | class ErrorHandlerMinimal { 6 | 7 | // The only public method 8 | public static function printError($msg) { 9 | 10 | if (php_sapi_name() == 'cli') { 11 | self::printErrorText($msg); 12 | } 13 | else { 14 | self::printErrorHtml($msg); 15 | } 16 | exit(); 17 | } 18 | 19 | private static function printErrorText($msg) { 20 | 21 | $out = preg_replace('/\s{2,}/', "\n\n", $msg); 22 | $out = "\n--- Error ---\n\n" . $out; 23 | 24 | print($out . "\n\n"); 25 | } 26 | 27 | private static function printErrorHtml($msg) { 28 | 29 | $style = '<style> body { margin: 0; padding: 0; } .tht-error { font-family: arial, sans-serif; padding: 0px 20px;} </style>'; 30 | $out = $style . '<div class="tht-error">' . "<h2>Error</h2>" . $msg . '</div>'; 31 | 32 | $out = preg_replace('/`(.*?)`/', '<code><b>$1</b></code>', $out); 33 | $out = preg_replace('/\n/', '<br>', $out); 34 | $out = preg_replace('#\s{2,}|//#', '<br><br>', $out); 35 | 36 | print($out); 37 | } 38 | } 39 | 40 | -------------------------------------------------------------------------------- /tht/lib/core/data/starterApp/config/app.jcon: -------------------------------------------------------------------------------- 1 | // App Config 2 | //-------------------------------------------------------------- 3 | { 4 | // Dynamic URL Routes 5 | // Each target path is relative to the `pages` directory. 6 | // See: https://tht.dev/reference/url-router 7 | //--------------------------------------------------- 8 | 9 | routes: { 10 | /examples/route-colors/{color}: examples/route-colors.tht 11 | } 12 | 13 | 14 | // Custom App Config 15 | // See: https://tht.dev/manual/module/config/get 16 | //--------------------------------------------------- 17 | 18 | app: { 19 | // myNumber: 1234 20 | // myBoolean: true 21 | // myString: Hello World 22 | } 23 | 24 | 25 | // Core THT Config 26 | // See: https://tht.dev/reference/app-config 27 | //--------------------------------------------------- 28 | 29 | tht: { 30 | // Print performance timing info 31 | // See https://tht.dev/reference/perf-panel 32 | showPerfPanel: false 33 | } 34 | 35 | 36 | // Email Config & Database Config 37 | //--------------------------------------------------- 38 | 39 | // See `config/app.local.jcon` 40 | } 41 | -------------------------------------------------------------------------------- /tht/lib/core/compiler/Symbol/Symbols/S_ClassPlugin.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | class S_ClassPlugin extends S_Statement { 6 | 7 | var $type = SymbolType::CLASS_PLUGIN; 8 | 9 | // e.g. plugin SomeClass, OtherClass 10 | function asStatement($p) { 11 | 12 | if (!$p->inClass) { 13 | $p->error('Keyword `' . $p->symbol->getValue() . '` should only appear within a class.'); 14 | } 15 | 16 | $p->space('*pluginS'); 17 | $p->next(); 18 | 19 | // Plugin list 20 | $plugins = []; 21 | while (true) { 22 | // $sClassName = $p->symbol; 23 | // if (! $sClassName->token[TOKEN_TYPE] === TokenType::WORD) { 24 | // $p->error("Expected a class name."); 25 | // } 26 | // $sClassName->updateType(SymbolType::PACKAGE); 27 | // $plugins []= $sClassName; 28 | 29 | $plugins []= $p->parseExpression(0); 30 | 31 | // $p->next(); 32 | if (!$p->symbol->isValue(",")) { break; } 33 | $p->space('x, ')->next(); 34 | } 35 | $sPlugins = $p->makeAstList(AstList::FLAT, $plugins); 36 | $this->addKid($sPlugins); 37 | 38 | return $this; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /tests/config/app.jcon: -------------------------------------------------------------------------------- 1 | // Test Site 2 | 3 | { 4 | routes: { 5 | /home: allTests.tht 6 | /test/{action}: testAction.tht 7 | /test-auto: userAccounts.tht @ autoFunction 8 | /benchmark/route/{pid}: benchmark/test-route.tht 9 | } 10 | 11 | tht: { 12 | 13 | // used by test 14 | timezone: America/Los_Angeles 15 | 16 | showPerfPanel: false 17 | _coreDevMode: false 18 | 19 | showErrorPageForMins: 20 20 | 21 | hitCounter: false 22 | 23 | _sendErrorsUrl: local 24 | 25 | litemarkCustomTags: { 26 | tag1: <b>CUSTOM: {1}</b> 27 | othertag1: <b>OTHER: {1}</b> 28 | } 29 | } 30 | 31 | app: { 32 | num: -123.45 33 | booleanFalse: false 34 | booleanTrue: true 35 | string: value with spaces, etc. 36 | map: { 37 | key: value 38 | } 39 | list: [ 40 | value 1 41 | value 2 42 | ] 43 | 44 | mlString: ''' 45 | line 1 46 | line 2 47 | ''' 48 | 49 | aUrl: https://asite.com 50 | aFile: /path/to/file.txt 51 | aDir: /some/path 52 | winFile: C:\some\path\file.txt 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /tht/lib/core/compiler/Symbol/Symbols/S_Dot.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | class S_Dot extends S_InfixSticky { 6 | 7 | var $type = SymbolType::MEMBER; 8 | 9 | function asLeft($p) { 10 | 11 | $p->next(); 12 | $this->space(' .x'); 13 | 14 | $sMember = $p->symbol; 15 | $sMember->updateType(SymbolType::MEMBER_VAR); 16 | 17 | $this->addKids([ $p->assignmentLeftSide, $sMember ]); 18 | $p->next(); 19 | 20 | return $this; 21 | } 22 | 23 | // Dot member. foo.bar 24 | function asInner($p, $objName) { 25 | 26 | $p->next(); 27 | $this->space('N.x'); 28 | $sMember = $p->symbol; 29 | 30 | if ($sMember->token[TOKEN_TYPE] !== TokenType::WORD) { 31 | 32 | ErrorHandler::addSubOrigin('dot'); 33 | 34 | $suggest = ''; 35 | if ($sMember->token[TOKEN_TYPE] == TokenType::VAR) { 36 | $suggest = 'Try: Square brackets. Ex: `$var[$key]`'; 37 | } 38 | 39 | $p->error("Expected a field name. Ex: `\$user.name` $suggest"); 40 | } 41 | 42 | $sMember->updateType(SymbolType::MEMBER_VAR); 43 | 44 | $this->addKids([ $objName, $sMember ]); 45 | $p->next(); 46 | 47 | return $this; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /tht/lib/core/compiler/Symbol/Symbols/S_New.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | // class S_New extends Symbol { 6 | 7 | // var $type = SymbolType::NEW_OBJECT; 8 | 9 | // // e.g. new Foo() 10 | // function asLeft($p) { 11 | 12 | // $p->space('*newS'); 13 | 14 | // $p->next(); 15 | 16 | // $sClassName = $p->symbol; 17 | // if (! $sClassName->token[TOKEN_TYPE] === TokenType::WORD) { 18 | // $p->error("Expected a class name. Ex: `new User()`"); 19 | // } 20 | // $p->space('SclassNamex'); 21 | // $sClassName->updateType(SymbolType::PACKAGE); 22 | // $this->addKid($sClassName); 23 | // $p->next(); 24 | 25 | // // Argument list 26 | // $p->now('(', 'new')->space('x(x')->next(); 27 | // $args = []; 28 | // while (true) { 29 | // if ($p->symbol->isValue(')')) { break; } 30 | // $args[]= $p->parseExpression(0); 31 | // if (!$p->symbol->isValue(",")) { break; } 32 | // $p->space('x, ')->next(); 33 | // } 34 | // $argSymbol = $p->makeAstList(AstList::FLAT, $args); 35 | // $this->addKid($argSymbol); 36 | 37 | // $p->now(')')->space('x)*')->next(); 38 | 39 | 40 | // return $this; 41 | // } 42 | // } 43 | -------------------------------------------------------------------------------- /tests/code/pages/misc/form-upload-image.tht: -------------------------------------------------------------------------------- 1 | 2 | @@.form = Form.create('uploadForm', { 3 | 4 | // uploadFile: { 5 | // type: 'file' 6 | // rule: 'file|maxSizeMb:1' 7 | // } 8 | uploadImage: { 9 | rule: 'image' 10 | dir: 'uploads/profiles' 11 | dim: '200x200' 12 | } 13 | }) 14 | 15 | fun main { 16 | 17 | $page = Page.create({ 18 | title: 'Upload Image' 19 | main: formHtml() 20 | css: [url'/css/basic.css'] 21 | js: [url'/js/form.js'] 22 | }) 23 | 24 | Output.sendPage($page) 25 | } 26 | 27 | fun postMode { 28 | 29 | // Validate input, then run the inner function 30 | @@.form.process(fun ($data) { 31 | 32 | $imageUrl = @@.form.getUploadedImage('uploadImage', 'uploads') 33 | 34 | >> $imageUrl 35 | 36 | return thanksHtml($imageUrl) 37 | }) 38 | } 39 | 40 | 41 | 42 | tem formHtml { 43 | 44 | <h1> Upload Image 45 | 46 | <.panel> 47 | --- $buttonLabel = Web.icon('check').append(html' Upload Image') 48 | {{ @@.form.toHtml($buttonLabel) }} 49 | </> 50 | 51 | } 52 | 53 | tem thanksHtml($imageUrl) { 54 | 55 | <p> Thanks, we got your upload! 56 | 57 | <img src="{{ $imageUrl }}"> 58 | 59 | <p> <a href="/examples/contact-form"> Back to Form 60 | } 61 | -------------------------------------------------------------------------------- /tht/lib/core/data/starterApp/code/public/css/app.css: -------------------------------------------------------------------------------- 1 | 2 | 3 | /* Variables 4 | ---------------------------------------------------------*/ 5 | 6 | :root { 7 | --headerColor: #ddd; 8 | } 9 | 10 | 11 | /* App Styles 12 | ---------------------------------------------------------*/ 13 | 14 | main { 15 | max-width: 768px; 16 | } 17 | 18 | header { 19 | padding: 1rem 2rem; 20 | background-color: var(--headerColor); 21 | font-weight: bold; 22 | } 23 | 24 | header a { 25 | text-decoration: none; 26 | color: #333; 27 | } 28 | 29 | footer { 30 | text-align: center; 31 | border-top: solid 1px var(--headerColor); 32 | margin-top: 6rem; 33 | padding: 2rem; 34 | } 35 | 36 | body { 37 | font-size: 2rem; 38 | color: #222; 39 | } 40 | 41 | h1 { 42 | color: #394; 43 | } 44 | 45 | .subline { 46 | width: 100%; 47 | font-size: 2.5rem; 48 | color: #394; 49 | margin-bottom: 4rem; 50 | margin-top: -3rem; 51 | border-bottom: solid 1px #d6d6e6; 52 | padding-bottom: 2rem; 53 | } 54 | 55 | code { 56 | font-weight: bold; 57 | } 58 | 59 | 60 | /* Mobile Overrides 61 | ---------------------------------------------------------*/ 62 | 63 | @media screen and (max-width: 768px) { 64 | 65 | .myClass { 66 | width: 100%; 67 | } 68 | 69 | } 70 | -------------------------------------------------------------------------------- /tht/lib/core/main/Error/ErrorPage/ErrorPageText.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | // TODO: include stack trace. Have to pull out from ErrorPageHtml 6 | // TODO: include source line. 7 | // TODO: include help link? 8 | class ErrorPageText { 9 | 10 | function print($error) { 11 | 12 | $out = $this->format($error); 13 | 14 | print($out); 15 | } 16 | 17 | function format($parts) { 18 | 19 | $out = "\n--- " . $parts['title'] . " ---\n\n"; 20 | 21 | if (Tht::isMode('web')) { 22 | $out .= 'URL: ' . THT::module('Request')->u_get_url()->u_render_string() . "\n"; 23 | $out .= 'Client IP: ' . THT::module('Request')->u_get_ip() . "\n\n"; 24 | } 25 | 26 | $parts['message'] = preg_replace("/ {2,}/", "\n\n", $parts['message']); 27 | $line = "..........................................................."; 28 | $out .= "$line\n\n\n" . $parts['message'] . "\n\n\n$line\n"; 29 | 30 | $source = $parts['source']; 31 | if ($source['lineNum'] >= 1) { 32 | $out .= "\nLine: " . $source['lineNum']; 33 | } 34 | 35 | if (isset($parts['filePath'])) { 36 | $out .= "\nFile: " . $parts['filePath']; 37 | } 38 | 39 | $out .= "\n\n"; 40 | 41 | return $out; 42 | } 43 | } -------------------------------------------------------------------------------- /tht/lib/core/compiler/Symbol/Symbols/S_Lambda.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | class S_Lambda extends S_Statement { 6 | 7 | use ClosureVarParser; 8 | 9 | var $isExpression = true; 10 | 11 | var $type = SymbolType::OPERATOR; 12 | 13 | function asLeft($p) { 14 | $p->next(); 15 | $this->space('*xx'); 16 | 17 | $p->expressionDepth += 1; // prevent assignment 18 | $p->lambdaDepth += 1; 19 | 20 | $p->now('{', 'lambda.open')->space('x{ ')->next(); 21 | $p->skipNewline(); 22 | $this->addKid($p->parseExpression(0)); 23 | 24 | // Allow outer vars to be used inside expression 25 | // All this is similar to S_Function 26 | $outerScopeVars = $p->validator->getAllInScope(); 27 | $p->validator->newFunctionScope(); 28 | $p->validator->newScope(); 29 | $closureVars = $this->parseClosureVars($p, $outerScopeVars); 30 | if ($closureVars) { 31 | $this->addKid($p->makeAstList(AstList::ARGS, $closureVars)); 32 | } 33 | 34 | $p->skipNewline(); 35 | $p->now('}', 'lambda.close')->space(' }N')->next(); 36 | 37 | 38 | $p->validator->popScope(); // function 39 | $p->validator->popFunctionScope(); 40 | 41 | $p->lambdaDepth -= 1; 42 | 43 | return $this; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /tht/lib/core/data/starterApp/code/pages/examples/hello-form.tht: -------------------------------------------------------------------------------- 1 | 2 | // See the manual on how Forms work here: 3 | // https://tht.dev/manual/module/form 4 | 5 | // This is called when the page is first requested. 6 | fun main { 7 | 8 | $page = Page.create({ 9 | css: [url'vendor/basic.css'] 10 | js: [url'vendor/form.js'] 11 | }) 12 | 13 | $form = getForm() 14 | $html = formHtml($form) 15 | $page.setMain($html) 16 | 17 | Output.sendPage($page) 18 | } 19 | 20 | // This is called when the form is submitted. 21 | fun postMode { 22 | 23 | $form = getForm() 24 | 25 | // Validate the input field. 26 | // If it's ok, then it runs the inner function. 27 | $form.process(fun ($data) { 28 | 29 | // Send HTML back to the browser, which will replace the form. 30 | return helloHtml($data.name) 31 | }) 32 | } 33 | 34 | fun getForm { 35 | 36 | // Create a form with a single 'text' input field. 37 | $form = Form.create('helloForm', { 38 | name: { tag: 'text', rule: 'name' } 39 | }) 40 | 41 | return $form 42 | } 43 | 44 | tem formHtml($form) { 45 | 46 | <h1> Hello Form 47 | 48 | {{ $form.toHtml('Submit') }} 49 | } 50 | 51 | tem helloHtml($name) { 52 | 53 | <p> Hello, <b>{{ $name }}</>! 54 | 55 | <p> <a href="hello-form"> Back to Form 56 | } 57 | -------------------------------------------------------------------------------- /tests/config/app.local.jcon: -------------------------------------------------------------------------------- 1 | { 2 | tht: { 3 | // Do NOT change this after your app goes live. 4 | // ALL of the URLs that contain scrambled IDs will change! 5 | // See: https://tht.dev/manual/class/string/scramble-num 6 | scrambleNumSecretKey: 5e1a0950 7 | } 8 | 9 | // Database settings 10 | // See: https://tht.help/manual/module/db 11 | databases: { 12 | 13 | // Default sqlite file in 'data/db' 14 | default: { 15 | driver: sqlite 16 | file: app.db 17 | } 18 | 19 | errors: { 20 | driver: sqlite 21 | file: errors.db 22 | } 23 | 24 | mapDb: { 25 | driver: sqlite 26 | file: qdb.db 27 | buckets: [ 28 | users 29 | ] 30 | } 31 | 32 | badDb: { 33 | driver: badDriver 34 | } 35 | 36 | mysqlDb: { 37 | driver: mysql 38 | server: localhost 39 | database: mysql 40 | username: root 41 | password: 42 | } 43 | 44 | } 45 | 46 | email: { 47 | host: smtp.sendgrid.net 48 | port: 587 49 | user: apikey 50 | password: <api_key> 51 | } 52 | 53 | app: { 54 | local: { 55 | localVar: 789 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /tht/lib/core/data/starterApp/code/pages/examples/upload-form.tht: -------------------------------------------------------------------------------- 1 | 2 | @@.form = Form.create('uploadForm', { 3 | 4 | resume: { 5 | tag: 'file' 6 | rule: { 7 | type: 'file' 8 | ext: 'doc,pdf,txt' 9 | dir: dir'app:/data/uploads/resumes' 10 | } 11 | help: 'Allowed files: doc, pdf, txt -- Under 2 MB' 12 | } 13 | }) 14 | 15 | 16 | // Runs when the page is first loaded. 17 | fun main { 18 | 19 | $page = Page.create({ 20 | title: 'Upload Form' 21 | main: formHtml() 22 | css: [url'/vendor/basic.css'] 23 | js: [url'/vendor/form.js'] 24 | }) 25 | 26 | Output.sendPage($page) 27 | } 28 | 29 | // Runs when the form is submitted (i.e. request method is POST) 30 | fun postMode { 31 | 32 | // Validate input, then run the inner function 33 | @@.form.process(fun ($data) { 34 | 35 | $resumePath = $data.resume 36 | 37 | // Replace form with HTML fragment 38 | return thanksHtml() 39 | }) 40 | } 41 | 42 | tem formHtml { 43 | 44 | <h1> Upload Form 45 | 46 | <.panel> 47 | 48 | --- $buttonLabel = Web.icon('upload').append(html' Upload Files') 49 | {{ @@.form.toHtml($buttonLabel) }} 50 | 51 | </> 52 | 53 | } 54 | 55 | tem thanksHtml { 56 | 57 | <p> The files were validated and uploaded to <code>data/uploads/resumes</>. 58 | 59 | <p> <a href="/examples/upload-form"> Back to Form 60 | } 61 | -------------------------------------------------------------------------------- /tht/lib/core/compiler/Symbol/Symbols/S_OpenSquare.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | class S_OpenSquare extends S_InfixSticky { 6 | 7 | // Dynamic member. foo[...] 8 | function asInner($p, $left) { 9 | 10 | $p->next(); 11 | $this->updateType(SymbolType::MEMBER); 12 | $this->space('x[x'); 13 | 14 | $this->addKids([$left, $p->parseExpression(0)]); 15 | $p->now(']', 'index.close')->space('x]*')->next(); 16 | 17 | return $this; 18 | } 19 | 20 | // List literal. [ ... ] 21 | function asLeft($p) { 22 | 23 | $sOpenBracket = $p->symbol; 24 | 25 | $p->next(); 26 | $this->updateType(SymbolType::AST_LIST); 27 | $els = []; 28 | 29 | $pos = 0; 30 | $isMultiline = false; 31 | 32 | while (true) { 33 | 34 | $isMultiline = $p->parseElementSeparator($pos, $isMultiline, ']'); 35 | $pos += 1; 36 | 37 | if ($p->symbol->isValue("]")) { 38 | break; 39 | } 40 | 41 | $els []= $p->parseExpression(0); 42 | 43 | if ($p->symbol->isValue(":")) { 44 | $p->error("Unexpected token in List: `:` Try: Convert `[…]` to `{…}`"); 45 | } 46 | } 47 | 48 | $sOpenBracket->space($isMultiline ? '*[B' : '*[x'); 49 | $p->space($isMultiline ? 'B]*' : 'x]*'); 50 | $p->next(); 51 | 52 | $this->addKids($els); 53 | 54 | return $this; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /tests/code/pages/misc/image-optim.tht: -------------------------------------------------------------------------------- 1 | 2 | fun main { 3 | 4 | $files = q[ 5 | tht_favicon.png 6 | lospec_continuum_773674.png 7 | oscar_the_grouch.png 8 | logo_social.png 9 | text.png 10 | landscape.jpeg 11 | gradient.png 12 | motorcycle_alpha.png 13 | ] 14 | 15 | if Input.get('css') { 16 | Output.sendCss(css()) 17 | } 18 | else { 19 | 20 | Output.sendHtml(pageHtml($files)) 21 | } 22 | } 23 | 24 | tem pageHtml($files) { 25 | 26 | <html> 27 | <head> <link rel="stylesheet" href="?css=true" /> 28 | 29 | <body> 30 | 31 | <style> body { background-color: #ddd; font-family: arial; } img { max-width: 600px } h2 { font-size: 300%; margin: 3rem 0 2rem; } 32 | 33 | <h2> Image Optimization Test 34 | 35 | <p> Optimization code is in `Image/ImageOptimizer.php`. 36 | 37 | <h2> Optimized 38 | --- foreach $files as $f { 39 | <div> <img src="{{ '/images/optimize/' ~ $f }}"> 40 | --- } 41 | 42 | <h2> Thumbs 43 | --- foreach $files as $f { 44 | --- $f = .replace('.', '_thumb200.') 45 | <div> <img src="{{ '/images/optimize/' ~ $f }}"> 46 | --- } 47 | 48 | <h2> As Is 49 | --- foreach $files as $f { 50 | --- $f = .replace('.', '_asis.') 51 | <div> <img src="{{ '/images/optimize/' ~ $f }}"> 52 | --- } 53 | 54 | </> 55 | </> 56 | } 57 | 58 | tem css { 59 | 60 | body { 61 | background: url(/images/optimize/bg_texture.png); 62 | } 63 | 64 | } -------------------------------------------------------------------------------- /tht/lib/core/compiler/Symbol/Symbols/S_If.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | class S_If extends S_Statement { 6 | 7 | var $type = SymbolType::OPERATOR; 8 | var $allowAsMapKey = true; 9 | 10 | // if / else 11 | function asStatement($p) { 12 | 13 | $this->space('*ifS'); 14 | 15 | // Start scope here so that new vars assigned (:=) inside condition 16 | // are captured in the underlying block. 17 | $p->validator->newScope(); 18 | 19 | $p->next(); 20 | 21 | $p->expressionDepth += 1; // prevent assignment 22 | $p->ifDepth += 1; 23 | 24 | // Condition. if ... { 25 | $sCondition = $p->parseExpression(0, 'noOuterParen'); 26 | $this->addKid($sCondition); 27 | 28 | $p->ifDepth -= 1; 29 | 30 | // block. { ... } 31 | $this->addKid($p->parseBlock(false, true)); 32 | 33 | // else/if 34 | if ($p->symbol->isValue('else')) { 35 | 36 | $p->space(' else*'); 37 | $p->next(); 38 | 39 | if ($p->symbol->isValue('if')) { 40 | // `else if` 41 | $p->space(' if '); 42 | $this->addKid($p->parseStatement()); 43 | } 44 | else { 45 | // final `else` 46 | $this->addKid($p->parseBlock()); 47 | } 48 | } 49 | else { 50 | Validator::validateUnsupportedKeyword($p->symbol->token, false); 51 | } 52 | 53 | return $this; 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /tht/lib/core/data/starterApp/config/app.local.jcon: -------------------------------------------------------------------------------- 1 | 2 | // IMPORTANT: 3 | // 4 | // This config file should be kept OUT of version control. 5 | //---------------------------------------------------------------- 6 | 7 | { 8 | tht: { 9 | // Do NOT change this after your app goes live. 10 | // ALL of the URLs that contain scrambled IDs will change! 11 | // See: https://tht.dev/manual/class/string/scramble-num 12 | scrambleNumSecretKey: __scrambleNumSecretKey__ 13 | } 14 | 15 | // Database Config 16 | // 17 | // See: https://tht.dev/manual/module/db 18 | //------------------------------------------------- 19 | 20 | databases: { 21 | 22 | // Default sqlite file in 'data/db' 23 | default: { 24 | driver: sqlite 25 | file: app.db 26 | } 27 | 28 | // Other database 29 | // Access via e.g. `Db.use('exampleDb')` 30 | 31 | // exampleDb: { 32 | // driver: mysql or pgsql 33 | // server: localhost 34 | // database: example 35 | // username: dbuser 36 | // password: s3cr3tw0rd 37 | // } 38 | } 39 | 40 | // Email config 41 | // 42 | // See: https://tht.dev/reference/email-config 43 | //------------------------------------------------- 44 | 45 | // email: { 46 | // host: smtp.sendgrid.net 47 | // port: 587 48 | // user: apikey 49 | // password: abc123def456abc123def456abc123def456 50 | // } 51 | } -------------------------------------------------------------------------------- /tests/code/modules/tests/lib/NetTests.tht: -------------------------------------------------------------------------------- 1 | 2 | public fun run($t) { 3 | 4 | $t.section('Module: Net') 5 | 6 | if $t.skipSlowTests(): return $t 7 | 8 | $head = Net.httpHead(url'http://example.com') 9 | 10 | $t.ok($head.status == 200, 'head - status') 11 | $t.ok($head.contentLength > 500, 'head - number') 12 | $t.ok($head.date.zClassName() == 'Date', 'head - date') 13 | 14 | $t.ok(Net.httpHead(url'https://badhost.blah').status == 0, 'head - bad host') 15 | 16 | 17 | Net.setTimeoutSecs(5) 18 | $t.ok(Net.httpStatus(url'https://httpstat.us/200?sleep=1000') == 200, 'timeout ok') 19 | $t.ok(Net.lastError() == '', 'no last error') 20 | 21 | Net.setTimeoutSecs(1) 22 | $t.ok(Net.httpStatus(url'https://httpstat.us/200?sleep=2000') == 0, 'timed out') 23 | $t.ok(Net.lastError().contains(rx'failed to open stream'i), 'last error') 24 | 25 | $content = Net.httpGet(url'https://tht.dev') 26 | $t.ok($content.match(rx'programming language'i), 'get request') 27 | 28 | $content = Net.httpPost( 29 | url'https://putsreq.com/TiktDvn26ykgGwi8GQ4M' 30 | { name: 'tht' } 31 | ) 32 | $t.ok('OK|tht', 'post request') 33 | $t.ok(Net.lastError() == '', 'last error reset') 34 | 35 | $t.ok(Net.httpStatus(url'https://tht.dev/install') == 200, 'urlExists - ok') 36 | $t.ok(Net.httpStatus(url'https://tht-nope.dev') == 0, 'urlExists - bad hostname') 37 | $t.ok(Net.httpStatus(url'https://tht.dev/sdfsdf') == 404, 'urlExists - missing path') 38 | 39 | return $t 40 | } 41 | -------------------------------------------------------------------------------- /tests/code/modules/tests/lib/PerfTests.tht: -------------------------------------------------------------------------------- 1 | 2 | public fun run($t) { 3 | 4 | if $t.skipSlowTests(): return $t 5 | 6 | $t.section('Module: Perf') 7 | 8 | // Perf.forceActive(true) 9 | // Perf.start('test.perfModule') 10 | // System.sleep(1) 11 | // Perf.stop(true) 12 | 13 | // $res = Perf.results(true) 14 | // $found = false 15 | // for (r in res.single) { 16 | // if (r.task == 'testPerf') { 17 | // found = true 18 | // break 19 | // } 20 | // } 21 | // $t.ok(found, 'Perf task & results') 22 | 23 | // Perf.forceActive(false) 24 | 25 | 26 | $t.section('Performance Speed Tests') 27 | 28 | $numIters = 1000 29 | 30 | // make sure array access doesn't hit performance 31 | // 0.57 ms 0.04 MB 32 | $perfTask = Perf.start('test.perf.largeArray') 33 | $start = Perf.now() 34 | $nums = Math.range(1, $numIters) 35 | foreach $nums as $nn { 36 | $a = $nums[$nn] 37 | } 38 | $elapsed = Perf.now() - $start 39 | $perfTask.stop() 40 | $t.ok($elapsed <= 1, 'ArrayAccess loop ({} elements) took <= 1 ms'.fill($numIters)) 41 | 42 | // 1 million = 40ms 43 | $perfTask = Perf.start('test.perf.rangeGenerator') 44 | $start = Perf.now() 45 | $gen = range(1, $numIters) 46 | foreach $gen as $n { 47 | // ... 48 | } 49 | $perfTask.stop() 50 | $elapsed = Perf.now() - $start 51 | $t.ok($elapsed <= 1, 'rangeGenerator loop ({} iters) took <= 1 ms'.fill($numIters)) 52 | 53 | return $t 54 | } 55 | -------------------------------------------------------------------------------- /tht/lib/stdlib/modules/_index.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | class StdLibModules { 6 | 7 | static public $files = [ 8 | 'File', 9 | 'Log', 10 | 'Test', 11 | 'Date', 12 | 'String', 13 | 'Test', 14 | 'Php', 15 | 'System', 16 | 'Json', 17 | 'Meta', 18 | 'Math', 19 | 'Result', 20 | 'Perf', 21 | 'Db', 22 | 'Web', 23 | 'Request', 24 | 'Output', 25 | 'Litemark', 26 | 'Jcon', 27 | 'Session', 28 | 'Cookie', 29 | 'Cache', 30 | 'Net', 31 | 'MapDb', 32 | 'Input', 33 | 'AppConfig', 34 | 'Bare', 35 | 'Form', 36 | 'Email', 37 | 'Page', 38 | 'Image', 39 | ]; 40 | 41 | public static function register() { 42 | 43 | // Register modules for autoloading when used 44 | foreach (self::$files as $lib) { 45 | ModuleManager::registerStdModule($lib); 46 | } 47 | 48 | ModuleManager::registerStdModule('Perf', new u_Perf ()); 49 | ModuleManager::registerStdModule('Regex', new u_Regex ()); 50 | ModuleManager::registerStdModule('Result', new u_Result ()); 51 | ModuleManager::registerStdModule('*Bare', new u_Bare ()); 52 | } 53 | 54 | public static function isa($className) { 55 | 56 | return in_array($className, self::$files); 57 | } 58 | } 59 | 60 | ModuleManager::initAutoloading(); 61 | 62 | StdLibModules::register(); 63 | 64 | -------------------------------------------------------------------------------- /tests/code/modules/tests/lib/SessionTests.tht: -------------------------------------------------------------------------------- 1 | 2 | public fun run($t) { 3 | 4 | $t.section('Module: Session') 5 | 6 | Session.deleteAll() 7 | 8 | Session.set('key1', 'value') 9 | Session.set('key2', { a: 'b' }) 10 | $t.ok(Session.get('key1') == 'value', 'set/get') 11 | $t.ok(Session.get('key2').a == 'b', 'get map') 12 | 13 | $t.ok(Session.getAll().keys().join('|') == 'key1|key2', 'getAll') 14 | 15 | $t.ok(Session.get('missing', '') == '', 'get with blank default') 16 | $t.ok(Session.get('missing', 'default') == 'default', 'get with default') 17 | 18 | $t.ok(Session.hasKey('key1'), 'hasKey true') 19 | $t.ok(Session.delete('key1') == 'value', 'delete') 20 | $t.ok(!Session.hasKey('key1'), 'hasKey false') 21 | 22 | Session.deleteAll() 23 | $t.ok(Session.getAll().keys().length() == 0, 'deleteAll') 24 | 25 | $t.ok(Session.addCounter('num') == 1, 'counter 1') 26 | $t.ok(Session.addCounter('num') == 2, 'counter 2') 27 | 28 | Session.setFlash('fkey', 'fvalue') 29 | $t.ok(Session.getFlash('fkey') == 'fvalue', 'flash set/get') 30 | 31 | $t.ok(Session.hasFlash('fkey'), 'hasFlash - true') 32 | $t.ok(Session.hasFlash('missing') == false, 'hasFlash - false') 33 | 34 | Session.addToList('list', 123) 35 | $t.ok(Session.get('list')[1] == 123, 'addToList 1') 36 | 37 | Session.addToList('list', 456) 38 | $t.ok(Session.get('list')[2] == 456, 'addToList 2') 39 | 40 | $t.dies(fun { Session.get('missing') }, 'get bad key', 'Unknown session key') 41 | 42 | return $t 43 | } 44 | -------------------------------------------------------------------------------- /tht/lib/stdlib/modules/Result.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | class u_Result extends OStdModule { 6 | 7 | function u_ok($v) { 8 | 9 | $this->ARGS('*', func_get_args()); 10 | 11 | return new OResult ($v, true, 0); 12 | } 13 | 14 | function u_fail($code='general') { 15 | 16 | $this->ARGS('s', func_get_args()); 17 | 18 | return new OResult ('', false, $code); 19 | } 20 | } 21 | 22 | class OResult extends OVar { 23 | 24 | private $rvalue = ''; 25 | private $code = 'general'; 26 | private $ok = true; 27 | 28 | function __construct($val, $ok, $code) { 29 | 30 | $this->rvalue = $val; 31 | $this->ok = $ok; 32 | $this->code = $code; 33 | } 34 | 35 | function __toString() { 36 | 37 | return '(Result: ' . ($this->ok ? $this->rvalue : 'FAIL:' . $this->code) . ')'; 38 | } 39 | 40 | function u_get($default=null) { 41 | 42 | $this->ARGS('*', func_get_args()); 43 | 44 | if (!$this->ok) { 45 | if ($default !== null) { 46 | return $default; 47 | } 48 | Tht::error("Result object is in a failure state. Check `failCode()` method first."); 49 | } 50 | 51 | return $this->rvalue; 52 | } 53 | 54 | function u_is_ok() { 55 | 56 | $this->ARGS('', func_get_args()); 57 | 58 | return $this->ok; 59 | } 60 | 61 | function u_get_fail_code() { 62 | 63 | $this->ARGS('', func_get_args()); 64 | 65 | return $this->code; 66 | } 67 | } 68 | 69 | -------------------------------------------------------------------------------- /tests/code/pages/misc/form-checkboxes.tht: -------------------------------------------------------------------------------- 1 | 2 | @@.form = Form.create('checkboxForm', { 3 | size: { 4 | tag: 'radio' 5 | options: { 6 | small: 'Small (12 inch)' 7 | medium: 'Medium (14 inch)' 8 | large: 'Large (16 inch)' 9 | } 10 | } 11 | toppings: { 12 | tag: 'checkbox' 13 | options: q[pepperoni sausage onions mushrooms anchovies] 14 | } 15 | }) 16 | 17 | 18 | // Runs when the page is first loaded. 19 | fun main { 20 | 21 | $page = Page.create({ 22 | title: 'Checkbox Form' 23 | main: formHtml() 24 | css: [url'/css/basic.css'] 25 | js: [url'/js/form.js'] 26 | }) 27 | 28 | Output.sendPage($page) 29 | } 30 | 31 | // Runs when the form is submitted (i.e. request method is POST) 32 | fun postMode { 33 | 34 | // Validate input, then run the inner function 35 | @@.form.process(fun ($data) { 36 | 37 | // Replace form with HTML fragment 38 | return thanksHtml($data) 39 | }) 40 | } 41 | 42 | tem formHtml { 43 | 44 | <h1> Checkbox Form 45 | 46 | <.panel> 47 | 48 | <h2> Pizza 49 | 50 | --- $buttonLabel = Web.icon('lock').append(html' Order Pizza') 51 | {{ @@.form.toHtml($buttonLabel) }} 52 | 53 | </> 54 | 55 | } 56 | 57 | tem thanksHtml($pizza) { 58 | 59 | <p> We are making your <b>{{ $pizza.size.toUpperCase(-first) }}</> pizza with: 60 | 61 | <ul> 62 | --- foreach $pizza.toppings as $pt { 63 | <li> <b> {{ $pt.toUpperCase(-first) }} 64 | --- } 65 | </> 66 | 67 | <p> <a href=""> Back to Form 68 | } 69 | -------------------------------------------------------------------------------- /tht/lib/stdlib/classes/OPassword.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | class OPassword extends OClass { 6 | 7 | private $plainText = ''; 8 | private $hash = ''; 9 | 10 | function __construct($plainText) { 11 | $this->plainText = $plainText; 12 | } 13 | 14 | function u_on_string_token() { 15 | 16 | return $this->u_x_danger_hash(); 17 | } 18 | 19 | function u_z_to_sql_string() { 20 | 21 | $this->ARGS('', func_get_args()); 22 | 23 | return $this->u_x_danger_hash(); 24 | } 25 | 26 | function u_length() { 27 | 28 | $this->ARGS('', func_get_args()); 29 | 30 | return mb_strlen($this->plainText); 31 | } 32 | 33 | function u_check_pattern($match) { 34 | 35 | $this->ARGS('*', func_get_args()); 36 | 37 | if (!ORegex::isa($match)) { 38 | $this->error("1st argument must be a Regex string `r'...'`"); 39 | } 40 | 41 | return v($this->plainText)->u_match($match) !== ''; 42 | } 43 | 44 | function u_check($correctHash) { 45 | 46 | $this->ARGS('s', func_get_args()); 47 | 48 | return Security::rateLimitedPasswordCheck($this->plainText, $correctHash); 49 | } 50 | 51 | function u_x_danger_hash() { 52 | 53 | $this->ARGS('', func_get_args()); 54 | 55 | if (!$this->hash) { 56 | $this->hash = Security::hashPassword($this->plainText); 57 | } 58 | 59 | return $this->hash; 60 | } 61 | 62 | function u_x_danger_plain_text() { 63 | 64 | $this->ARGS('', func_get_args()); 65 | 66 | return $this->plainText; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /tht/lib/core/data/starterApp/code/pages/examples/checkbox-form.tht: -------------------------------------------------------------------------------- 1 | 2 | @@.form = Form.create('checkboxForm', { 3 | size: { 4 | tag: 'radio' 5 | options: { 6 | small: 'Small (12 inch)' 7 | medium: 'Medium (14 inch)' 8 | large: 'Large (16 inch)' 9 | } 10 | } 11 | toppings: { 12 | tag: 'checkbox' 13 | options: q[ pepperoni sausage onions mushrooms anchovies ] 14 | } 15 | }) 16 | 17 | 18 | // Runs when the page is first loaded. 19 | fun main { 20 | 21 | $page = Page.create({ 22 | title: 'Checkbox Form' 23 | main: formHtml() 24 | css: [url'/vendor/basic.css'] 25 | js: [url'/vendor/form.js'] 26 | }) 27 | 28 | Output.sendPage($page) 29 | } 30 | 31 | // Runs when the form is submitted (i.e. request method is POST) 32 | fun postMode { 33 | 34 | // Validate input, then run the inner function 35 | @@.form.process(fun ($data) { 36 | 37 | // Replace form with HTML fragment 38 | return thanksHtml($data) 39 | }) 40 | } 41 | 42 | tem formHtml { 43 | 44 | <h1> Checkbox Form 45 | 46 | <.panel> 47 | 48 | <h2> Pizza 49 | 50 | --- $buttonLabel = Web.icon('lock').append(html' Order Pizza') 51 | {{ @@.form.toHtml($buttonLabel) }} 52 | 53 | </> 54 | 55 | } 56 | 57 | tem thanksHtml($pizza) { 58 | 59 | <p> We are making your <b>{{ $pizza.size.toUpperCase(-first) }}</> pizza with: 60 | 61 | <ul> 62 | --- foreach $pizza.toppings as $pt { 63 | <li> <b> {{ $pt.toUpperCase(-first) }} 64 | --- } 65 | </> 66 | 67 | <p> <a href="/examples/checkbox-form"> Back to Form 68 | } 69 | -------------------------------------------------------------------------------- /tht/lib/core/compiler/Symbol/Symbols/S_Ternary.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | class S_Ternary extends Symbol { 6 | var $type = SymbolType::TERNARY; 7 | var $bindingPower = 20; 8 | 9 | // e.g. test ? result1 : result2 10 | function asInner($p, $left) { 11 | 12 | $sQuestion = $p->symbol; 13 | 14 | $p->next(); 15 | 16 | if ($p->inTernary) { 17 | $p->error("Nested ternary operator not allowed: `\$a ? \$b : \$c` Try: `if/else`"); 18 | } 19 | $p->inTernary = true; 20 | 21 | $this->addKid($left); 22 | $this->space(' ? '); 23 | 24 | $result1 = $p->symbol->token; 25 | $this->addKid($p->parseExpression(0)); 26 | 27 | $p->now(':', 'ternary.colon')->space(' : ')->next(); 28 | 29 | $result2 = $p->symbol->token; 30 | $this->addKid($p->parseExpression(0)); 31 | 32 | $p->inTernary = false; 33 | 34 | // Teachable moment 35 | if ($result1[TOKEN_TYPE] == TokenType::WORD && $result2[TOKEN_TYPE] == TokenType::WORD) { 36 | $vals = $result1[TOKEN_VALUE] . '|' . $result2[TOKEN_VALUE]; 37 | if ($vals == 'true|false') { 38 | $p->error('Unnecessary ternary. You can just use a standalone boolean expression. Try: (example) `$a == $b` instead of `$a == $b ? true : false`', $sQuestion->token); 39 | } 40 | else if ($vals == 'false|true') { 41 | $p->error('Unnecessary ternary. You can just use a standalone boolean expression. Try: (example) `$a != $b` instead of `$a == $b ? false : true`', $sQuestion->token); 42 | } 43 | } 44 | 45 | return $this; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /tht/lib/core/compiler/Symbol/Symbols/S_InfixSticky.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | class S_InfixSticky extends Symbol { 6 | 7 | var $bindingPower = 80; 8 | var $type = SymbolType::INFIX; 9 | 10 | function asInner($p, $left) { 11 | 12 | $infixValue = $p->symbol->getValue(); 13 | 14 | if ($infixValue == '=>') { 15 | $p->error('Invalid operator: `=>` Try: `>=` (greater or equal)'); 16 | } 17 | 18 | $this->space(' + ', 'infix'); 19 | $p->next(); 20 | 21 | if ($p->symbol->isNewline()) { 22 | $v = $p->symbol->getValue(); 23 | $p->error("Unexpected newline. Try: Put `$infixValue` on next line to continue statement."); 24 | } 25 | 26 | $right = $p->parseExpression($this->bindingPower); 27 | if (!$right) { 28 | $p->error('Missing right operand.'); 29 | } 30 | 31 | $this->addKids([$left, $right]); 32 | 33 | return $this; 34 | } 35 | } 36 | 37 | // ~ 38 | class S_Concat extends S_InfixSticky { 39 | var $bindingPower = 50; 40 | var $type = SymbolType::OPERATOR; 41 | } 42 | 43 | // +, -, etc. 44 | class S_Add extends S_InfixSticky { 45 | var $bindingPower = 51; 46 | 47 | // Unary + and - 48 | function asLeft($p) { 49 | $this->space('*!x'); 50 | $p->next(); 51 | $this->updateType(SymbolType::PREFIX); 52 | $this->addKids([$p->parseExpression(70)]); 53 | return $this; 54 | } 55 | } 56 | 57 | // *, /, etc. 58 | class S_Multiply extends S_InfixSticky { 59 | var $bindingPower = 52; 60 | } 61 | 62 | // **, etc. 63 | class S_Power extends S_InfixSticky { 64 | var $bindingPower = 53; 65 | } 66 | 67 | -------------------------------------------------------------------------------- /tests/code/modules/tests/lib/MetaTests.tht: -------------------------------------------------------------------------------- 1 | 2 | public fun run($t) { 3 | 4 | $t.section('Module: Meta') 5 | 6 | .ok(Meta.functionExists('metaCallMe'), 'functionExists') 7 | .ok(Meta.callFunction('metaCallMe', ['a', 'b']) == 'a|b', 'callFunction & arguments') 8 | 9 | .ok(Meta.failIfTemplateMode() == null, 'failIfTemplateMode ok') 10 | .dies(fun { failModeHtml() }, 'can`t be called in Template mode') 11 | .dies(fun { templateFailOutputHtml() }, 'can`t be called in Template mode') 12 | .dies(fun { templateFailInputHtml() }, 'can`t be called in Template mode') 13 | 14 | .ok(Meta.functionExists('dynamicFunction'), 'dynamic fun exists') 15 | .ok(Meta.callFunction('dynamicFunction', ['Hey']) == 'Hey!!!', 'call dynamic function') 16 | 17 | .ok(Meta.getThtVersion().match(rx'\d+\.\d+\.\d+'), 'thtVersion') 18 | .ok(Meta.getThtVersion(-num).match(rx'^\d{5}$'), 'thtVersion - digits') 19 | 20 | if !$t.skipSlowTests() { 21 | $t.ok(Meta.zGetStdLib()['File'].read.contains('$file.read'), 'zGetStdLib') 22 | } 23 | 24 | return $t 25 | } 26 | 27 | fun trigger($x, $y) { 28 | 29 | $x = 456 30 | 31 | $triggerError = fun { 32 | return 123 33 | } 34 | } 35 | 36 | fun metaCallMe($arg1, $arg2) { 37 | 38 | return $arg1 ~ '|' ~ $arg2 39 | } 40 | 41 | fun failTemplateMode { 42 | Meta.failIfTemplateMode() 43 | } 44 | 45 | tem failModeHtml { 46 | --- failTemplateMode() 47 | } 48 | 49 | fun dynamicFunction($a) { 50 | return $a ~ '!!!' 51 | } 52 | 53 | tem templateFailOutputHtml { 54 | 55 | --- Output.sendPage({}) 56 | } 57 | 58 | tem templateFailInputHtml { 59 | 60 | --- Input.get('num', 'i') 61 | } 62 | 63 | -------------------------------------------------------------------------------- /tht/lib/core/compiler/templateTransformers/LmTemplateTransformer.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | class LmTemplateTransformer extends TemplateTransformer { 6 | 7 | private $indent = 0; 8 | 9 | // preserve code within code fences, so there is no need to escape anything 10 | function transformNext() { 11 | $r = $this->reader; 12 | $c = $r->char1; 13 | 14 | if ($r->atStartOfLine()) { 15 | $this->indent = $r->slurpChar(' '); 16 | $c = $r->char1; 17 | if ($r->isGlyph("```")) { 18 | $s = $r->getLine() . "\n"; 19 | while (true) { 20 | $line = $r->slurpLine(); 21 | if (!$line) { 22 | // TODO: find actual line number 23 | Tht::error('Missing closing code fence in Litemark template.'); 24 | } 25 | $s .= $line['fullText'] . "\n"; 26 | if (substr($line['text'], 0,3) == "```" || $line['text'] === null) { 27 | break; 28 | } 29 | } 30 | return $s; 31 | } 32 | } 33 | else if ($c === "`") { 34 | $c = "`" . $r->slurpUntil('`') . $r->char1; 35 | $r->next(); 36 | return $c; 37 | } 38 | 39 | $r->next(); 40 | return $c; 41 | } 42 | 43 | function onEndChunk($s) { 44 | $str = Tht::module('Litemark')->parseWithFullPerms($s)->u_render_string(); 45 | 46 | // This messes up leading whitespace in <pre> tags. 47 | // $str = HtmlTemplateTransformer::cleanHtmlSpaces($str); 48 | 49 | return $str; 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /tests/code/modules/tests/lib/FormTests.tht: -------------------------------------------------------------------------------- 1 | 2 | public fun run($t) { 3 | 4 | $t.section('Module: Form') 5 | 6 | $form = Form.create('testForm', { 7 | num: { 8 | tag: 'number' 9 | rule: 'i|max:100' 10 | } 11 | comment: { 12 | tag: 'textarea' 13 | } 14 | email: { 15 | tag: 'email' 16 | } 17 | password: { 18 | tag: 'password' 19 | } 20 | }) 21 | 22 | $fhtml = $form.toHtml('Submit').renderString() 23 | 24 | $currUrl = Request.getUrl().getPath() 25 | $t.ok( 26 | $fhtml.contains('<form method="post" action="' ~ $currUrl ~ '" id="testForm"') 27 | 'form render - form tag' 28 | ) 29 | 30 | $t.ok( 31 | $fhtml.contains('<input aria-label="Num" name="num" value="0" type="number"') 32 | 'form render - num input tag' 33 | ) 34 | 35 | $t.ok( 36 | $fhtml.contains(''' 37 | <input aria-label="Email" autocomplete="email" name="email" value="" type="email" 38 | ''') 39 | 'form render - email input tag' 40 | ) 41 | 42 | $t.ok( 43 | $fhtml.contains('"csrfToken":') 44 | 'form render - csrfToken' 45 | ) 46 | 47 | $t.ok( 48 | $fhtml.contains('type="submit">Submit</button>') 49 | 'form render - submit button' 50 | ) 51 | 52 | $t.ok( 53 | $fhtml.contains('<span>Show Password</span>') && $fhtml.contains(''' 54 | <input aria-label="Password" autocomplete="current-password" name="password" value="" type="password" 55 | ''') 56 | 'form render - password' 57 | ) 58 | 59 | 60 | 61 | // TODO: more tests! 62 | 63 | return $t 64 | } 65 | 66 | 67 | -------------------------------------------------------------------------------- /tests/code/modules/tests/lang/TypeStringTests.tht: -------------------------------------------------------------------------------- 1 | 2 | public fun run($t) { 3 | 4 | $t.section('TypeStrings') 5 | 6 | $t.dies(fun { return plain'a' ~ 'b' }, 'Can`t string-append') 7 | $t.dies(fun { return 'a' ~ plain'b' }, 'Can`t string-append') 8 | $t.dies(fun { return url'a' ~ cmd'b' }, 'Can`t string-append') 9 | 10 | // $lock1 = plain'1={},'.fill('a') 11 | // $lock2 = plain'2={}'.fill('b') 12 | // $combined = $lock1 ~ $lock2 13 | // $t.ok($combined.renderString() == '1=a,2=b', 'combined TypeStrings') 14 | 15 | // $t1 = plain't1' 16 | // $t1 ~= plain't2' 17 | // // $t.ok($t1.renderString() == 't1t2', 'combined with ~=') 18 | 19 | $t.ok(tagHtml('a').stringType() == 'html', 'stringType') 20 | $t.ok(sql'x'.stringType() == 'sql', 'stringType') 21 | 22 | $t.dies( 23 | fun { return url'page?foo={}' } 24 | 'dynamic query hardcoded in url string', 'dynamic queries' 25 | ) 26 | 27 | $lUrl = url'http://test.com/'.setQuery({ foo: 'val`s' }) 28 | $lCmd = cmd'xget {} > file.txt'.fill($lUrl) 29 | $lHtml = deepEscHtml($lCmd) 30 | 31 | 32 | $escOut = '<b>xget 'http://test.com/?' ~ 'foo=val%27s' > file.txt</b>' 33 | if System.getOs() == 'windows' { 34 | // Windows escapes shell differently 35 | $escOut = '<b>xget "http://test.com/?foo=val 27s" > file.txt</b>' 36 | } 37 | $t.ok($lHtml.renderString() == $escOut, 'recursive escaped renderString()') 38 | 39 | $t.ok(hasDefault(123).renderString() == '<b>123</b>', 'as default arg') 40 | 41 | return $t 42 | } 43 | 44 | tem tagHtml($val) { 45 | <p> {{ $val }} 46 | } 47 | 48 | 49 | tem deepEscHtml($val) { 50 | <b> {{ $val }} 51 | } 52 | 53 | fun hasDefault($fillVal, $str = html'<b>{}</b>') { 54 | 55 | return $str.fill($fillVal) 56 | 57 | } 58 | 59 | -------------------------------------------------------------------------------- /tests/code/modules/tests/lang/TypeTests.tht: -------------------------------------------------------------------------------- 1 | 2 | public fun run($t) { 3 | 4 | $t.section('Types') 5 | 6 | $t.ok([].zClassName() == 'List', 'List') 7 | $t.ok({}.zClassName() == 'Map', 'Map') 8 | $t.ok('foo'.zClassName() == 'String', 'String') 9 | 10 | $n = 123 11 | $t.ok($n.zClassName() == 'Number', 'Number') 12 | 13 | $f = true 14 | $t.ok($f.zClassName() == 'Boolean', 'Boolean') 15 | 16 | $fnn = fun { } 17 | $t.ok($fnn.zClassName() == 'Function', 'Function') 18 | 19 | $t.ok(sql'abc'.zClassName() == 'SqlTypeString', 'SqlTypeString') 20 | $t.ok(rx'a'.zClassName() == 'Regex', 'Regex') 21 | $t.ok(fnNoReturn() == null, 'no return is null') 22 | 23 | 24 | // load('oop/BaseObject') 25 | 26 | // $b = BaseObject({}) 27 | // $t.ok($b.type() == 'object', 'object') 28 | // $t.ok('abc'.zClassName() == 'String', 'class: String') 29 | // $t.ok([1, 2, 3].zClassName() == 'List', 'class: List') 30 | // $t.ok(html'abc'.zClassName() == 'HtmlTypeString', 'class: HtmlTypeString') 31 | // $t.ok($b.zClassName() == 'BaseObject', 'class: Base') 32 | 33 | 34 | $t.section('Types - .equals') 35 | 36 | .ok('a'.equals('a'), 'string - true') 37 | .ok(!'a'.equals('b'), 'string - false') 38 | .ok(!'1'.equals(1), 'string to num - false') 39 | .ok(!'1'.equals(1 == 1), 'string to bool - false') 40 | .ok((false).equals(1 == 0), 'bool - true') 41 | 42 | .ok({}.equals({}), 'map to map - true') 43 | .ok({ a: 1 }.equals({ a: 1 }), 'map to map - true') 44 | .ok(!{ a: 1 }.equals({ a: 2 }), 'map to map - false') 45 | 46 | .ok({ a: 1, b: 2 }.equals({ b: 2, a: 1 }), 'map key order - true') 47 | 48 | .ok(![].equals({}), 'list to map - false') 49 | 50 | 51 | 52 | return $t 53 | } 54 | 55 | fun fnNoReturn { 56 | 57 | } 58 | 59 | -------------------------------------------------------------------------------- /tests/code/modules/tests/lib/WebTests.tht: -------------------------------------------------------------------------------- 1 | 2 | public fun run($t) { 3 | 4 | $t.section('Module: Web') 5 | 6 | $t.ok(Web.skipHitCounter() == true, 'skipHitCounter - true') 7 | 8 | $t.ok(Web.icon('arrowRight').renderString().contains('ticon'), 'icon') 9 | $t.dies(fun { Web.icon('missingBlah') }, 'icon - error', 'Unknown icon') 10 | 11 | $em = Web.maskEmail('user@example.com') 12 | $t.ok($em.renderString().match(rx'@.*?display:none'), 'mask email') 13 | 14 | $postHtml = Web.postLink(html'Delete', url'/delete', { id: 123 }, 'delete-button') 15 | .renderString() 16 | $m1 = '<button type="submit" class="delete-button">Delete</button>' 17 | $m2 = '<form method="post" action="/delete"' 18 | $t.ok($postHtml.contains($m1) && $postHtml.contains($m2), 'postLink') 19 | 20 | 21 | $m1 = '<link rel="stylesheet" href="/assets/app.css" />' 22 | $t.ok(Web.cssTag(url'/assets/app.css').renderString() == $m1, 'cssTag') 23 | 24 | $al = Web.anchor('Oranges, etc.').renderString() 25 | $t.ok($al == '<a name="oranges-etc"></a>', 'anchor') 26 | 27 | $al = Web.anchor('Oranges, etc.', -link).renderString() 28 | $m = '<a href="#oranges-etc" class="anchor-link">#</a><a name="oranges-etc"></a>' 29 | $t.ok($al == $m, 'anchor w self-link') 30 | 31 | $al = Web.anchor('Oranges, etc.', { link, linkLabel: 'link' }).renderString() 32 | $m = '<a href="#oranges-etc" class="anchor-link">link</a><a name="oranges-etc"></a>' 33 | $t.ok($al == $m, 'anchor w self-link & label') 34 | 35 | $al = Web.anchorUrl('Oranges, etc.').renderString() 36 | $t.ok($al == '#oranges-etc', 'anchorUrl') 37 | 38 | // TODO 39 | // >> Web.htmx('getThing', { id: 123 }).renderString() 40 | 41 | $t.ok(Web.nonce().length() == 40, 'nonce') 42 | $t.ok(Web.csrfToken().length() == 32, 'csrfToken') 43 | 44 | return $t 45 | } 46 | 47 | -------------------------------------------------------------------------------- /tests/code/modules/tests/lib/JconTests.tht: -------------------------------------------------------------------------------- 1 | 2 | public fun run($t) { 3 | 4 | $t.section('Module: Jcon') 5 | 6 | $d = Jcon.parse('{\nkey: value\n}\n') 7 | $t.ok($d.key == 'value', 'string value') 8 | 9 | $d = Jcon.parse('{\n key: "hello"\n }') 10 | $t.ok($d.key == 'hello', 'quoted string value') 11 | 12 | $d = Jcon.parse('{\nkey: true\n}\n') 13 | $t.ok($d.key == true, 'true value') 14 | 15 | $d = Jcon.parse('{\nkeyA: valA\nkeyB: valB\n}\n') 16 | $t.ok($d.keyB == 'valB', '2nd key') 17 | 18 | $d = Jcon.parse('{\nkey: false\n}\n') 19 | $t.ok($d.key == false, 'false value') 20 | 21 | $d = Jcon.parse('{\nkey: 1234.5\n}\n') 22 | $t.ok($d.key == 1234.5, 'num value') 23 | 24 | $d = Jcon.parse('{\nkey: [\nv1\nv2\nv3\n]\n}\n') 25 | $t.ok($d.key.length() == 3, 'list value') 26 | $t.ok($d.key[3] == 'v3', 'list value') 27 | 28 | $d = Jcon.parse('{\nkey: \'\'\'\nThis is\nmultiline\n\'\'\'\n}\n') 29 | $t.ok($d.key.contains('\nmultiline'), 'multiline value') 30 | 31 | $d = Jcon.parse('{\nkeLm: \'\'\'\n## Heading!\n\'\'\'\n}\n') 32 | $t.ok($d.keLm.renderString().contains('<h2>'), 'Litemark value') 33 | 34 | $t 35 | .ok(Jcon.fileExists('app.jcon'), 'fileExists') 36 | .ok(!Jcon.fileExists('missing.jcon'), 'fileExists - not') 37 | 38 | .dies(fun { Jcon.parse('sdfsdf') }, 'missing top-level') 39 | .dies(fun { Jcon.parse('{ foo: 123 }') }, 'Missing newline after open brace') 40 | .dies(fun { Jcon.parse('{\n foo: 123, \n}') }, 'remove trailing comma') 41 | .dies(fun { Jcon.parse('{\n foo: 1\n foo: 2\n }') }, 'duplicate key') 42 | .dies(fun { Jcon.parse('{\n foo : 1\n }') }, 'extra space before colon') 43 | .dies(fun { Jcon.parse('{\n foo:1\n }') }, 'missing space after colon') 44 | .dies(fun { Jcon.parse('') }, 'empty string') 45 | 46 | return $t 47 | } 48 | -------------------------------------------------------------------------------- /tht/lib/core/data/starterApp/code/pages/examples/ajax-weather.tht: -------------------------------------------------------------------------------- 1 | fun main { 2 | 3 | $page = Page.create({ 4 | title: 'Ajax Example' 5 | css: [url'/vendor/basic.css'] 6 | js: [url'https://unpkg.com/htmx.org@1.5.0'] 7 | }) 8 | 9 | $html = mainHtml() 10 | $page.setMain($html) 11 | 12 | Output.sendPage($page) 13 | } 14 | 15 | // See https://htmx.org/reference/ for how the "hx-" parameters are used. 16 | tem mainHtml { 17 | 18 | <h1> Ajax Example 19 | 20 | --- foreach weatherJcon() as $city/$weather { 21 | <button {{ Web.htmx('getWeather', { city: $city }) }} hx-target="#result"> 22 | {{ $weather.city }} 23 | </> 24 | --- } 25 | 26 | <.panel id="result" style="margin-top: 2rem"></> 27 | 28 | <small> This example uses <a href="https://htmx.org">HTMX</> for Ajax functionality. 29 | } 30 | 31 | // Try adding your own city. 32 | tem weatherJcon { 33 | { 34 | sanJose: { 35 | city: San Jose, USA 36 | temp: Nice & Warm 37 | chanceRain: Very Low 38 | } 39 | 40 | helsinki: { 41 | city: Helsinki, Finland 42 | temp: Cold & Freezing 43 | chanceRain: Medium 44 | } 45 | 46 | lima: { 47 | city: Lima, Peru 48 | temp: Hot & Humid 49 | chanceRain: Very High 50 | } 51 | } 52 | } 53 | 54 | // Handler for ajax request. Comes from the 'mode=getWeather' parameter. 55 | // See https://tht.dev/reference/page-modes 56 | fun getWeatherMode { 57 | 58 | $cityName = Input.post('city') 59 | 60 | $weather = weatherJcon() 61 | $weatherForCity = $weather[$cityName] 62 | 63 | return weatherHtml($weatherForCity) 64 | } 65 | 66 | tem weatherHtml($weather) { 67 | 68 | <h3> {{ $weather.city }} 69 | 70 | <p> Temperature: <b>{{ $weather.temp }}</b> 71 | <p> Chance of Rain: <b>{{ $weather.chanceRain }}</b> 72 | } 73 | -------------------------------------------------------------------------------- /tht/lib/core/data/starterApp/code/pages/examples/contact-form.tht: -------------------------------------------------------------------------------- 1 | 2 | @@.form = Form.create('contactForm', { 3 | 4 | name: { 5 | tag: 'text' 6 | rule: 'name' 7 | } 8 | email: { 9 | tag: 'email' 10 | rule: 'email|optional' 11 | help: 'We won`t send you junk mail.' 12 | } 13 | message: { 14 | tag: 'textarea' 15 | rule: 'comment|min:10|max:100' 16 | } 17 | priority: { 18 | tag: 'select' 19 | rule: 'id' 20 | options: { 21 | bug: 'Bug Report' 22 | suggestion: 'Suggestion' 23 | general: 'General Feedback' 24 | } 25 | } 26 | accept: { 27 | tag: 'checkbox' 28 | rule: 'accepted' 29 | label: 'I accept this form.' 30 | } 31 | }) 32 | 33 | 34 | // Runs when the page is first loaded. 35 | fun main { 36 | 37 | $page = Page.create({ 38 | title: 'Contact Form' 39 | main: formHtml() 40 | css: [url'/vendor/basic.css'] 41 | js: [url'/vendor/form.js'] 42 | }) 43 | 44 | Output.sendPage($page) 45 | } 46 | 47 | // Runs when the form is submitted (i.e. request method is POST) 48 | fun postMode { 49 | 50 | // Validate input, then run the inner function 51 | @@.form.process(fun ($data) { 52 | 53 | // Example of custom validation 54 | if $data.name == 'troll' { 55 | return ['name', 'Please go somewhere else.'] 56 | } 57 | 58 | Log.info($data) 59 | 60 | // Replace form with HTML fragment 61 | return thanksHtml($data.name) 62 | }) 63 | } 64 | 65 | tem formHtml { 66 | 67 | <h1> Contact Form 68 | 69 | <.panel> 70 | --- $buttonLabel = Web.icon('check').append(html' Contact Us') 71 | {{ @@.form.toHtml($buttonLabel) }} 72 | </> 73 | } 74 | 75 | tem thanksHtml($name) { 76 | 77 | <p> Thanks <b>{{ $name }}</>, we will contact you soon! 78 | 79 | <p> The form data was written to <code>data/logs/app.log</> 80 | 81 | <p> <a href="/examples/contact-form"> Back to Form 82 | } 83 | -------------------------------------------------------------------------------- /tests/code/modules/tests/lib/PhpTests.tht: -------------------------------------------------------------------------------- 1 | 2 | public fun run($t) { 3 | 4 | $t.section('Module: Php') 5 | 6 | $t.ok(Php.getVersion().match(rx'\d+\.\d+\.\d+'), 'PHP version') 7 | $t.ok(Php.getVersion(-num) >= 50500, 'PHP version - numeric') 8 | 9 | $fl = Php.options(['PATHINFO_FILENAME', 'PATHINFO_BASENAME']) 10 | $t.ok($fl == 10, 'PHP - constant flags') 11 | 12 | $t.ok(Php.getConstant('E_CORE_ERROR') == 16, 'getConstant') 13 | 14 | $t.ok(Php.call('strrev', 'abcdef') == 'fedcba', 'call') 15 | $t.dies( 16 | fun { Php.call('nonexistent', 1, 2) }, 'Non-existent PHP call' 17 | 'PHP function does not exist' 18 | ) 19 | $t.dies( 20 | fun { Php.call('eval', 'echo("hi");') }, 'stop blocklisted fun - by name' 21 | 'PHP function is blocklisted' 22 | ) 23 | $t.dies( 24 | fun { Php.call('ini_set', 'x', 'y') }, 'stop blocklisted fun - by match' 25 | 'PHP function is blocklisted' 26 | ) 27 | 28 | 29 | Php.require('vendor/testVendorClass.php') 30 | 31 | $vc = Php.new('Abc/VendorClass') 32 | $t.ok($vc.takeArray([1, 2, 3]) == 1, 'Vendor class - take array') 33 | $t.ok($vc.returnArray([1, 2, 3])[1] == 'a', 'Vendor class - return array') 34 | $t.ok($vc.returnRecords().remove(1)['color'] == 'Red', 'Vendor class - recursive arrays') 35 | $t.ok($vc.returnObject().callMe() == 'abc', 'Vendor subClass') 36 | 37 | $t.ok($vc.zSet('ALL_CAP_FIELD', 789), 'Vendor class - ALL_CAP_FIELD') 38 | $t.ok($vc.zGet('ALL_CAP_FIELD') == 789, 'Vendor class - ALL_CAP_FIELD') 39 | $t.ok($vc.zCall('ALL_CAP_METHOD') == 'FOO', 'Vendor class - ALL_CAP_METHOD') 40 | 41 | $t.dies(fun { $v = Php.version }, 'version()', 'Try: call method `version()`') 42 | 43 | $t.ok(Php.functionExists('strpos'), 'fun exists') 44 | $t.ok(!Php.functionExists('strposxx'), 'fun exists (not)') 45 | $t.ok(Php.classExists('DateTime'), 'class exists') 46 | $t.ok(!Php.classExists('FooBar'), 'class exists (not)') 47 | $t.ok(Php.classExists('/o/u_Test'), 'class exists (o namespace)') 48 | 49 | $t.ok(Php.call('Abc/VendorClass::staticFunction', 123) == 'STATIC: 123', 'static call') 50 | 51 | return $t 52 | } 53 | -------------------------------------------------------------------------------- /tests/code/modules/tests/lib/PageTests.tht: -------------------------------------------------------------------------------- 1 | 2 | public fun run($t) { 3 | 4 | $t.section('Module: Page') 5 | 6 | $page = Page.create({ 7 | appName: 'AppName' 8 | joiner: '~' 9 | tagline: 'This is a tagline' 10 | }) 11 | 12 | $page.addBodyClass('class1') 13 | $page.addBodyClass('otherClass') 14 | $page.setDescription('This is the desc. ') 15 | $page.setIcon(url'/images/icon.png') 16 | $page.setImage(url'/images/image.png') 17 | $page.addToHead(html'<meta name="custom" property="THT">') 18 | 19 | // Files have to exist 20 | $page.addCss(url'/css/basic.css') 21 | $page.addJs(url'/js/form.js') 22 | 23 | $out = $page.toHtml() 24 | 25 | $t.ok($out.match(rx'<!doctype html>\s*<html>\s*<head>'s), 'start tags') 26 | $t.ok($out.match(rx'<head>.*</head>'s), 'head tags') 27 | $t.ok($out.match(rx'</body>\s*</html>'s), 'end tags') 28 | 29 | $title = '<title>AppName ~ This is a tagline' 30 | $t.ok($out.renderString().contains($title), 'title - default') 31 | $t.ok($out.contains('"og:title" content="This is a tagline"'), 'tagline to og:title') 32 | 33 | $page.setTitle('Page Title') 34 | 35 | $out = $page.toHtml().renderString() 36 | 37 | $title = '<title>Page Title ~ AppName' 38 | $t.ok($out.contains($title), 'title - after setTitle') 39 | 40 | $t.ok($out.contains('"og:title" content="Page Title"'), 'og:title') 41 | $t.ok($out.contains('"og:site_name" content="AppName"'), 'og:site_name') 42 | $t.ok($out.contains('meta name="viewport"'), 'viewport meta tag') 43 | 44 | $t.ok($out.match(rx''' 45 | <head>.*<meta name="custom" property="THT">.*</head> 46 | '''s), 'addToHead') 47 | 48 | // TODO: Test for min.gz and cache 'v' param 49 | $t.ok($out.match(rx'"og:image" content="/images/image\.png"'), 'image') 50 | $t.ok($out.match(rx'<link rel="icon" href="/images/icon\.png">'), 'icon') 51 | $t.ok($out.match(rx'rel="stylesheet" href="/css/basic\.css"'), 'addCss') 52 | $t.ok($out.match(rx''' 53 | <script src="/js/form\.js" nonce="[a-zA-Z0-9]{20,}"> 54 | '''), 'addJs') 55 | 56 | $t.dies(fun { Page.create({ foo: 123 }) }, 'invalid page field') 57 | 58 | return $t 59 | } 60 | -------------------------------------------------------------------------------- /tht/lib/core/compiler/Symbol/Symbols/S_Class.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | class S_Class extends S_Statement { 6 | 7 | var $type = SymbolType::NEW_CLASS; 8 | 9 | var $allowAsMapKey = true; 10 | 11 | // e.g. class Foo { ... } 12 | function asStatement($p) { 13 | 14 | if ($p->numClasses) { 15 | $p->error('Only one class allowed per file.'); 16 | } 17 | $p->numClasses += 1; 18 | 19 | $p->next(); 20 | 21 | // Class name 22 | $sName = $p->symbol; 23 | $this->space('*classS'); 24 | if ($sName->token[TOKEN_TYPE] != TokenType::WORD) { 25 | $p->error("Expected a class name. Ex: `class User { ... }`"); 26 | } 27 | else { 28 | $sName->updateType(SymbolType::PACKAGE); 29 | $this->addKid($sName); 30 | } 31 | 32 | $p->next(); 33 | 34 | // X these out, but need to keep for now because Emitter expects these symbols downstream 35 | $this->readParentPackage($p, 'XextendsX'); 36 | $this->readParentPackage($p, 'XimplementsX'); 37 | 38 | // class block 39 | $p->inClass = true; 40 | $this->addKid($p->parseBlock()); 41 | $p->inClass = false; 42 | 43 | return $this; 44 | } 45 | 46 | function readParentPackage($p, $relation) { 47 | 48 | if ($p->symbol->isValue($relation)) { 49 | 50 | // TODO allow comma 51 | $p->next(); 52 | $sRelationClassName = $p->symbol; 53 | if ($sRelationClassName->token[TOKEN_TYPE] !== TokenType::WORD) { 54 | $p->error("Expected a class name. Ex: `class MyClass $relation OtherClass { ... }`"); 55 | } 56 | $sRelationClassName->updateType(SymbolType::FULL_PACKAGE); 57 | $this->addKid($sRelationClassName); 58 | 59 | $p->next(); 60 | } 61 | else { 62 | $this->addEmptyKid($p); 63 | } 64 | } 65 | 66 | function addEmptyKid($p) { 67 | $sNull = $p->makeSymbol( 68 | TokenType::WORD, 69 | '', 70 | SymbolType::FULL_PACKAGE 71 | ); 72 | $this->addKid($sNull); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /tht/lib/stdlib/modules/Form.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | require_once('Form/FormObject.php'); 6 | 7 | 8 | // Form Module 9 | class u_Form extends OStdModule { 10 | 11 | private $forms = []; 12 | 13 | private $strings = [ 14 | 'optional' => 'Optional', 15 | 'firstSelectOption' => 'Select...', 16 | 'showPassword' => 'Show Password', 17 | 'passwordHelp' => '✓ 8+ letters   ✓ At least 1 number or symbol' 18 | ]; 19 | 20 | function u_create($formId, $formSchema) { 21 | 22 | $this->ARGS('sm', func_get_args()); 23 | 24 | if (preg_match('/[^a-zA-Z0-9\-]/', $formId)) { 25 | $this->error("Form ID should contain only letters and numbers and dashes. Got: `" . $formId . "`"); 26 | } 27 | 28 | foreach ($formSchema as $k => $v) { 29 | if (!OMap::isa($v)) { 30 | $this->error("Form schema field `$k` must contain a Map."); 31 | } 32 | } 33 | 34 | $form = new Form ($formId, $formSchema); 35 | $this->forms[$formId] = $form; 36 | 37 | return $form; 38 | } 39 | 40 | function u_set_help_strings($map=null) { 41 | 42 | $this->ARGS('m', func_get_args()); 43 | 44 | if (!$map) { return $this->strings; } 45 | 46 | $map = unv($map); 47 | foreach ($map as $k => $v) { 48 | 49 | if (!isset($this->strings[$k])) { 50 | $this->error("Invalid Form string key: `$k`"); 51 | } 52 | 53 | $this->strings[$k] = $v; 54 | } 55 | } 56 | 57 | function getString($key) { 58 | 59 | return $this->strings[$key]; 60 | } 61 | 62 | function u_get_submitted_form_id() { 63 | 64 | $this->ARGS('', func_get_args()); 65 | 66 | $postData = Tht::getPhpGlobal('post', '*'); 67 | 68 | return isset($postData['formId']) ? ('' . $postData['formId']) : ''; 69 | } 70 | 71 | function u_csrf_tag() { 72 | 73 | $this->ARGS('', func_get_args()); 74 | 75 | $t = Security::getCsrfToken(); 76 | 77 | $tag = '<input type="hidden" name="csrfToken" value="' . $t . '" />'; 78 | 79 | return new HtmlTypeString($tag); 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /tests/code/modules/tests/lib/JsonTests.tht: -------------------------------------------------------------------------------- 1 | 2 | public fun run($t) { 3 | 4 | $t.section('Module: Json') 5 | 6 | .ok(Json.decode(json'{"k1":[123,"hello"]}')['k1'][2] == 'hello', 'decode sub-list') 7 | .ok(Json.decode(json'{"k1":{"k2":"hello"}}')['k1']['k2'] == 'hello', 'decode sub-map') 8 | .ok(Json.decode(json'[1,2,3]')[2] == 2, 'decode list') 9 | .ok(Json.decode(json'true') == true, 'decode boolean') 10 | .ok(Json.decode(json'123.45') == 123.45, 'decode number') 11 | 12 | $st = Json.encode({ a: 'hi', b: [1, 2, 3] }) 13 | 14 | $t 15 | .ok($st.renderString().contains('"hi"'), 'encode string') 16 | .ok($st.renderString().contains('[1,2,3]'), 'encode list') 17 | .ok($st.renderString().contains('"b":'), 'encode key') 18 | .dies(fun { Json.decode(json'{"a":123,/*"b":"comment"*/}') }, 'unable to decode') 19 | .dies(fun { Json.decode(json'{"test\ud800":123}') }, 'unable to decode') 20 | .dies(fun { Json.decode(json'{"te\st":123}') }, 'unable to decode') 21 | .dies(fun { Json.decode(json'{"a":123}=') }, 'unable to decode') 22 | .dies(fun { Json.decode(json'{"a":NaN}') }, 'unable to decode') 23 | .dies(fun { Json.decode(json'{"a":Inf}') }, 'unable to decode') 24 | .dies(fun { Json.decode(json'{"a":1.0e4096}') }, 'invalid large number') 25 | 26 | $obj = Json.decode($st) 27 | $t.ok($obj.b[2] == 2, 'decode after encode') 28 | 29 | $t.ok(Json.validate(json'{"a":[1,2,3]}'), 'validate - true') 30 | $t.ok(!Json.validate(json'{"a}'), 'validate - false') 31 | 32 | $jsonFile = file'files:/test.json' 33 | $jsonFile.delete(-ifExists) 34 | $writeData = { a: [1, 2, 3] } 35 | Json.writeFile($jsonFile, $writeData) 36 | $readData = Json.readFile($jsonFile) 37 | $t.ok($readData.a.join('|') == '1|2|3', 'Json - readFile & writeFile') 38 | 39 | $t.dies(fun { Json.writeFile($jsonFile, Result.ok(1)) }, 'unable to encode JSON') 40 | $jsonFile.write('{"a"') 41 | $t.dies(fun { Json.readFile($jsonFile) }, 'unable to decode JSON') 42 | 43 | // Waiting for PHP security patch 44 | // $dupe = json'{"a":123,"a":999}' 45 | // >> $dupe 46 | // >> Json.decode($dupe) 47 | 48 | return $t 49 | } 50 | -------------------------------------------------------------------------------- /tht/lib/stdlib/modules/Cookie.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | class u_Cookie extends OStdModule { 6 | 7 | private $localCache = []; 8 | 9 | function u_set($key, $value) { 10 | 11 | $this->ARGS('ss', func_get_args()); 12 | 13 | if (preg_match('/[^a-zA-Z0-9]/', $value)) { 14 | $this->error('Cookie value may only contain alphanumeric characters (a-zA-Z0-9).'); 15 | } 16 | 17 | $this->validateKey($key); 18 | 19 | $options = [ 20 | 'expires' => time() + 30 * 24 * 3600, // 30 days 21 | 'path' => '/', 22 | 'domain' => '', 23 | 'secure' => Security::isDev() ? false : true, 24 | 'httponly' => true, 25 | 'samesite' => 'Lax', 26 | ]; 27 | 28 | setcookie($key, $value, $options); 29 | 30 | $this->localCache[$key] = $value; 31 | 32 | return NULL_NORETURN; 33 | } 34 | 35 | function u_get($key) { 36 | 37 | $this->ARGS('s', func_get_args()); 38 | 39 | $this->validateKey($key); 40 | 41 | if (isset($this->localCache[$key])) { 42 | return $this->localCache[$key]; 43 | } 44 | 45 | return Tht::getPhpGlobal('cookie', $key, ''); 46 | } 47 | 48 | function u_delete($key) { 49 | 50 | $this->ARGS('s', func_get_args()); 51 | 52 | $this->validateKey($key); 53 | 54 | $options = [ 55 | 'expires' => -3600, 56 | 'path' => '/', 57 | 'domain' => '', 58 | ]; 59 | 60 | setcookie($key, '', $options); 61 | 62 | unset($this->localCache[$key]); 63 | 64 | return NULL_NORETURN; 65 | } 66 | 67 | function validateKey($key) { 68 | 69 | if (preg_match('/[^a-zA-Z0-9]/', $key)) { 70 | $this->error("Cookie key may only contain alphanumeric characters (a-zA-Z0-9). Got: `$key`"); 71 | } 72 | 73 | if (strlen($key) > 40) { 74 | $this->error("Cookie key length must be 40 characters or less. Got: `$key`"); 75 | } 76 | 77 | $sessionKey = Tht::module('Session')->sessionIdName; 78 | if ($key == $sessionKey) { 79 | $this->error("Read/write to Session cookie `$sessionKey` is restricted."); 80 | } 81 | } 82 | 83 | } 84 | 85 | 86 | -------------------------------------------------------------------------------- /tht/lib/core/compiler/Symbol/Symbols/S_ForEach.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | class S_ForEach extends S_Statement { 6 | 7 | var $type = SymbolType::OPERATOR; 8 | 9 | var $allowAsMapKey = true; 10 | 11 | // for (...) { ... } 12 | function asStatement($p) { 13 | 14 | $this->space('*foreachS'); 15 | 16 | $p->expressionDepth += 1; // prevent assignment 17 | 18 | $sFor = $p->symbol; 19 | $p->next(); 20 | 21 | // catch outer parens 22 | $sOuterParen = $p->symbol->isValue('(') ? $p->symbol : null; 23 | if ($sOuterParen) { $p->next(); } 24 | 25 | // iterator 26 | $sIter = $p->parseExpression(0); 27 | $this->addKid($sIter); 28 | 29 | $p->now('as', 'foreach.as'); 30 | $p->validator->newScope(); 31 | $p->next(); 32 | 33 | // Item variable. foreach ($list as $item) { ... } 34 | if ($p->symbol->type !== SymbolType::USER_VAR) { 35 | $p->error('Expected a list variable. Ex: `foreach $list as $item { ... }`'); 36 | } 37 | 38 | $p->validator->defineVar($p->symbol, true); 39 | $this->addKid($p->symbol); 40 | 41 | $peekToken = $p->peekNextToken(); 42 | if ($peekToken[TOKEN_VALUE] == '=>') { 43 | $p->error('Unknown token: `=>` Try: `foreach $map as $k/$v { ... }`', $peekToken); 44 | } 45 | 46 | 47 | $p->next(); 48 | 49 | // `$key/$value` alias. foreach ($map as $k/$v) { ... } 50 | if ($p->symbol->isValue('/')) { 51 | $p->space('x/x')->next(); 52 | if ($p->symbol->type !== SymbolType::USER_VAR) { 53 | $p->error('Expected a key/value pair. Ex: `foreach $users as $userName/$age { ... }`'); 54 | } 55 | $p->validator->defineVar($p->symbol, true); 56 | $this->addKid($p->symbol); 57 | $p->next(); 58 | } 59 | 60 | if ($p->symbol->isValue(')') && $sOuterParen) { 61 | $p->outerParenError($sOuterParen); 62 | } 63 | 64 | $p->breakableDepth += 1; 65 | $this->addKid($p->parseBlock(true)); 66 | $p->breakableDepth -= 1; 67 | 68 | // Make sure next symbol is out of this scope 69 | $p->validator->popScope(); // block 70 | $p->validator->popScope(); // foreach 71 | $p->next(); 72 | 73 | return $this; 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /tht/lib/core/compiler/Symbol/Symbols/S_Unsupported.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | class S_Unsupported extends Symbol { 6 | 7 | var $allowAsMapKey = true; 8 | 9 | static public function getSuggestion($token) { 10 | 11 | return [ 12 | 13 | 'let' => 'Remove keyword: `let`', 14 | 15 | 'class' => '(TBD)', 16 | 'switch' => '`if/else` or Map', 17 | 'while' => ['`loop { ... }`', 'Loops', '/language-tour/loops#infinite-loops'], 18 | 'for' => '`foreach $list as $item {`', 19 | 20 | 'require' => ['`load`', 'Modules', '/language-tour/custom-modules'], 21 | 'include' => ['`load`', 'Modules', '/language-tour/custom-modules'], 22 | 'static' => ['Module-level variable or function', 'Modules', '/language-tour/custom-modules'], 23 | 24 | 'final' => ['Remove `final` keyword.', 'Classes & Objects', '/language-tour/oop/classes-and-objects'], 25 | 'protected' => ['Remove `protected` keyword.', 'Classes & Objects', '/language-tour/oop/classes-and-objects'], 26 | 'abstract' => ['Remove `abstract` keyword.', 'Classes & Objects', '/language-tour/oop/classes-and-objects'], 27 | 'new' => ['Remove `new` keyword.', 'Classes & Objects', '/language-tour/oop/classes-and-objects'], 28 | 29 | 'private/class' => ['Remove keyword. Methods are private is default.', 'Classes & Objects', '/language-tour/classes-and-objects'], 30 | 'private/module' => ['Add `public` to other functions.', 'Modules', '/language-tour/custom-modules'], 31 | 32 | ][$token]; 33 | } 34 | 35 | function error($p) { 36 | 37 | $tokenVal = $this->token[TOKEN_VALUE]; 38 | if ($tokenVal == 'private') { 39 | $tokenVal = $this->parser->inClass ? 'private/class' : 'private/module'; 40 | } 41 | 42 | $try = self::getSuggestion($tokenVal); 43 | if (is_array($try)) { 44 | ErrorHandler::setHelpLink($try[2], $try[1]); 45 | $try = $try[0] ? "Try: " . $try[0] : ''; 46 | } 47 | else { 48 | $try = 'Try: ' . $try; 49 | } 50 | 51 | $p->error("Unsupported keyword: `" . $tokenVal . "` $try"); 52 | } 53 | 54 | function asStatement($p) { 55 | $this->error($p); 56 | } 57 | 58 | function asLeft($p) { 59 | $this->error($p); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /tht/lib/stdlib/modules/AppConfig.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | class u_AppConfig extends OClass { 6 | 7 | function u_get($key, $default = null) { 8 | 9 | $this->ARGS('s*', func_get_args()); 10 | 11 | $value = Tht::getAppConfig($key, $default); 12 | 13 | return $value; 14 | } 15 | 16 | // function u_get_dir($key, $default = null) { 17 | 18 | // $this->ARGS('s*', func_get_args()); 19 | 20 | // $val = Tht::getAppConfig($key, $default); 21 | 22 | // return $val !== '' ? new DirTypeString($val) : null; 23 | // } 24 | 25 | // function u_get_file($key, $default = '') { 26 | 27 | // $this->ARGS('s*', func_get_args()); 28 | 29 | // $val = Tht::getAppConfig($key, $default); 30 | 31 | // return $val !== '' ? new FileTypeString($val) : null; 32 | // } 33 | 34 | 35 | // function checkType($key, $val, $wantType) { 36 | // $gotType = v($val)->u_type(); 37 | 38 | // if ($gotType !== $wantType) { 39 | // if ($wantType == 'string' && $gotType == 'number') { 40 | // return; 41 | // } 42 | // $this->error("Config key `$key` expected type: `$wantType` Got: `$gotType`"); 43 | // } 44 | // } 45 | 46 | // function u_get_type_string($stringType, $key, $default = null) { 47 | 48 | // $this->ARGS('sss', func_get_args()); 49 | 50 | // $val = $this->u_get_string($key, $default); 51 | 52 | // return $val !== '' ? OTypeString::create($stringType, $val) : ''; 53 | // } 54 | 55 | // function u_get_string($key, $default = '') { 56 | 57 | // $this->ARGS('ss', func_get_args()); 58 | 59 | // $val = Tht::getAppConfig($key, $default); 60 | 61 | // $this->checkType($key, $val, 'string'); 62 | 63 | // return '' + $val; 64 | // } 65 | 66 | // function u_get_number($key, $default = null) { 67 | 68 | // $this->ARGS('sn', func_get_args()); 69 | 70 | // $val = Tht::getAppConfig($key, $default); 71 | 72 | // $this->checkType($key, $val, 'number'); 73 | 74 | // return 0 + $val; 75 | // } 76 | 77 | // function u_get_boolean($key, $default = false) { 78 | 79 | // $this->ARGS('sb', func_get_args()); 80 | 81 | // $val = Tht::getAppConfig($key, $default); 82 | 83 | // $this->checkType($key, $val, 'boolean'); 84 | 85 | // return $val; 86 | // } 87 | 88 | 89 | } 90 | -------------------------------------------------------------------------------- /tht/lib/stdlib/modules/Bare.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | // Functions without a module namespace 6 | class u_Bare extends OStdModule { 7 | 8 | static $FUNCTIONS = [ 'load', 'print', 'range', 'die' ]; 9 | 10 | static function isa($word) { 11 | 12 | return in_array($word, u_Bare::$FUNCTIONS); 13 | } 14 | 15 | function u_print() { 16 | 17 | $out = $this->formatPrint(func_get_args()); 18 | 19 | if (Tht::isMode('web')) { 20 | PrintPanel::add($out); 21 | } 22 | else { 23 | echo $out, "\n"; 24 | } 25 | 26 | return NULL_NORETURN; 27 | } 28 | 29 | function formatPrintedObject($a) { 30 | 31 | $a = v($a)->u_z_to_print_string(); 32 | 33 | if (Tht::isMode('web')) { 34 | if (HtmlTypeString::isa($a)) { 35 | $a = $a->u_render_string(); 36 | } 37 | else { 38 | $a = htmlentities($a); 39 | } 40 | } 41 | 42 | return $a; 43 | } 44 | 45 | function formatPrint($parts) { 46 | 47 | $outs = []; 48 | foreach ($parts as $a) { 49 | $outs []= $this->formatPrintedObject($a); 50 | } 51 | 52 | return implode("\n", $outs); 53 | } 54 | 55 | function u_load($relPath) { 56 | 57 | $this->ARGS('s', func_get_args()); 58 | 59 | return ModuleManager::loadUserModule($relPath); 60 | } 61 | 62 | // PERF: This is about 5x slower than a flat C-style for loop. 63 | // I tried flattening in PhpEmitter, but handling both ascending and descending ranges got complicated. 64 | // TODO: Revisit flattening to standard loop in the Emitter. This isn't nearly as common as iterating over a List, at least. 65 | function u_range($start, $end, $step=1) { 66 | 67 | $this->ARGS('nnN', func_get_args()); 68 | 69 | $i = ONE_INDEX - 1; 70 | if ($start < $end) { 71 | for ($num = $start; $num <= $end; $num += $step) { 72 | $i += 1; 73 | yield $i => $num; 74 | } 75 | } else { 76 | for ($num = $start; $num >= $end; $num -= $step) { 77 | $i += 1; 78 | yield $i => $num; 79 | } 80 | } 81 | } 82 | 83 | function u_die($msg, $data=null) { 84 | 85 | $this->ARGS('s*', func_get_args()); 86 | 87 | ErrorHandler::addOrigin('die'); 88 | Tht::error($msg, $data); 89 | } 90 | } 91 | 92 | -------------------------------------------------------------------------------- /tht/lib/core/compiler/Symbol/Symbols/S_Literal.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | class S_Literal extends Symbol { 6 | 7 | function asLeft($p) { 8 | 9 | $p->next(); 10 | 11 | return $this; 12 | } 13 | } 14 | 15 | // TODO: Name and Var are probably not actually "literals". 16 | class S_Name extends S_Literal { 17 | var $allowAsMapKey = true; 18 | } 19 | 20 | class S_Var extends S_Literal { 21 | } 22 | 23 | class S_Constant extends S_Literal { 24 | 25 | var $type = SymbolType::CONSTANT; 26 | 27 | function asLeft($p) { 28 | 29 | $p->next(); 30 | 31 | if ($this->isValue('@')) { 32 | if (!$p->inClass && !$p->anonFunctionDepth && !$p->lambdaDepth) { 33 | $p->error("Can't use `@` outside of an object.", $this->token); 34 | } 35 | } 36 | 37 | return $this; 38 | } 39 | } 40 | 41 | 42 | 43 | class S_Boolean extends S_Literal { 44 | var $type = SymbolType::BOOLEAN; 45 | var $preventYoda = true; 46 | } 47 | 48 | class S_Number extends S_Literal { 49 | var $type = SymbolType::NUMBER; 50 | var $allowAsMapKey = true; 51 | var $preventYoda = true; 52 | } 53 | 54 | class S_Null extends S_Literal { 55 | var $type = SymbolType::NULL; 56 | var $allowAsMapKey = true; 57 | var $preventYoda = true; 58 | } 59 | 60 | class S_Flag extends S_Literal { 61 | 62 | var $type = SymbolType::FLAG; 63 | var $preventYoda = true; 64 | 65 | function asLeft($p) { 66 | 67 | $p->next(); 68 | 69 | // TODO: this should be moved upstream, like in the Tokenizer 70 | $p->validator->validateFlagFormat($this->getValue(), $this->token); 71 | 72 | return $this; 73 | } 74 | } 75 | 76 | 77 | // Strings 78 | 79 | class S_String extends S_Literal { 80 | 81 | var $allowAsMapKey = true; 82 | var $preventYoda = true; 83 | 84 | // Don't let strings match internal parser values. e.g. else != 'else' 85 | function isValue($val) { 86 | return false; 87 | } 88 | 89 | var $type = SymbolType::STRING; 90 | } 91 | 92 | class S_TString extends S_String { 93 | var $type = SymbolType::T_STRING; 94 | var $allowAsMapKey = false; 95 | } 96 | 97 | class S_TemString extends S_String { 98 | var $type = SymbolType::TEM_STRING; 99 | var $allowAsMapKey = false; 100 | } 101 | 102 | class S_RxString extends S_String { 103 | var $type = SymbolType::RX_STRING; 104 | var $allowAsMapKey = false; 105 | } 106 | 107 | -------------------------------------------------------------------------------- /tests/code/modules/tests/lib/CacheTests.tht: -------------------------------------------------------------------------------- 1 | 2 | public fun run($t) { 3 | 4 | if $t.skipSlowTests(): return $t 5 | 6 | $t.section('Module: Cache') 7 | 8 | Cache.forceFileDriver() 9 | 10 | Cache.delete('test') 11 | Cache.set('test', 123, 1) 12 | 13 | $t.ok(Cache.has('test'), 'has') 14 | $t.ok(Cache.get('test') == 123, 'get') 15 | 16 | Cache.delete('not1') 17 | Cache.delete('not2') 18 | Cache.delete('not3') 19 | Cache.delete('not4') 20 | 21 | $t.ok(!Cache.has('not1'), 'has not') 22 | $t.ok(Cache.get('not1', 'missing') == 'missing', 'get default') 23 | 24 | $v = Cache.get('not2', fun { 25 | return 'fromSetter' 26 | }) 27 | $t.ok($v == 'fromSetter', 'get with default function') 28 | 29 | Cache.set('not3', 'fromSetter', 0) 30 | $t.ok(Cache.get('not3') == 'fromSetter', 'get with default function') 31 | 32 | $t.dies( 33 | fun { Cache.get('not4', fun { }) } 34 | 'must return a non-null value' 35 | ) 36 | 37 | 38 | 39 | Cache.set('data', { a: ['x', 'y', 'z'] }, 3) 40 | $t.ok(Cache.get('data').a.join('|') == 'x|y|z', 'get map + list') 41 | 42 | Cache.delete('data') 43 | $t.ok(!Cache.has('data'), 'delete') 44 | 45 | $t.ok(Cache.counter('count') == 1, 'counter 1') 46 | $t.ok(Cache.counter('count') == 2, 'counter 2') 47 | $t.ok(Cache.counter('count', 2) == 4, 'counter +2') 48 | $t.ok(Cache.counter('count', -1) == 3, 'counter -1') 49 | 50 | Cache.delete('count') 51 | 52 | Cache.set('short', 'xyz', 1) 53 | Cache.set('med', 'xyz', 10) 54 | Cache.set('forever', 'xyz', 0) 55 | 56 | Cache.clearLocalCache() 57 | System.sleep(2000) 58 | 59 | $t.ok(!Cache.has('short'), '1s expiry') 60 | $t.ok(Cache.get('short') == '', '1s expiry val') 61 | 62 | $t.ok(Cache.has('med'), '10s expiry') 63 | $t.ok(Cache.get('med') == 'xyz', 'no expiry val') 64 | $t.ok(Cache.has('forever'), 'no expiry') 65 | $t.ok(Cache.get('forever') == 'xyz', 'no expiry val') 66 | 67 | Cache.delete('short') 68 | Cache.delete('med') 69 | Cache.delete('forever') 70 | 71 | Cache.set('filePath', file'file.txt', 2) 72 | $t.ok(Cache.get('filePath').stringType() == 'file', 'FileTypeString') 73 | 74 | $map = { 75 | date: Date.now() 76 | url: url'https://example.com' 77 | } 78 | Cache.set('typeStringMap', $map, 2) 79 | 80 | $ret = Cache.get('typeStringMap') 81 | $t.ok($ret.date.zClassName() == 'Date', 'map with Date') 82 | $t.ok($ret.url.isTypeString() && $ret.url.stringType() == 'url', 'map with UrlTypeString') 83 | 84 | return $t 85 | } 86 | -------------------------------------------------------------------------------- /tht/lib/core/main/Tht/ThtInit.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | define('ONE_INDEX', 1); 6 | 7 | trait ThtInit { 8 | 9 | private static $APP_ENV_VAR = 'APP_ENV'; 10 | private static $REQUIRED_PHP_LIBS = ['mbstring', 'fileinfo', 'gd']; 11 | 12 | static private function checkRequirements() { 13 | 14 | if (PHP_VERSION_ID < self::$REQUIRE_PHP_VERSION_DIGITS) { 15 | Tht::startupError("PHP version " . self::$REQUIRE_PHP_VERSION_STRING . " or higher is required.\n\nCurrent Version: " . phpversion()); 16 | } 17 | 18 | foreach (self::$REQUIRED_PHP_LIBS as $lib) { 19 | self::checkRequiredPhpLib($lib); 20 | } 21 | } 22 | 23 | static public function checkRequiredPhpLib($lib, $altHelpMsg = '') { 24 | 25 | if (!extension_loaded($lib)) { 26 | Tht::phpLibError($lib, $altHelpMsg); 27 | } 28 | } 29 | 30 | static private function repairPhpIni() { 31 | 32 | $iniFile = php_ini_loaded_file() ?: ''; 33 | if (!$iniFile) { 34 | $prodIni = PHP_CONFIG_FILE_PATH . '/php.ini-production'; 35 | if (file_exists($prodIni)) { 36 | $iniFile = PHP_CONFIG_FILE_PATH . '/php.ini'; 37 | copy($prodIni, $iniFile); 38 | } 39 | else { 40 | // ??? 41 | } 42 | } 43 | 44 | $iniContent = file_get_contents($iniFile); 45 | $iniContent = preg_replace('/;(extension=(mbstring|fileinfo))/', '$1'); 46 | 47 | file_put_contents($iniFile, $iniContent); 48 | 49 | } 50 | 51 | static private function initRequestData() { 52 | 53 | Tht::$data['requestData'] = Security::initRequestData(); 54 | } 55 | 56 | static private function initMode() { 57 | 58 | $sapi = php_sapi_name(); 59 | 60 | Tht::$mode['testServer'] = $sapi === 'cli-server'; 61 | Tht::$mode['cli'] = $sapi === 'cli'; 62 | Tht::$mode['web'] = !Tht::$mode['cli']; 63 | } 64 | 65 | static private function loadLibs() { 66 | 67 | self::loadLib('lib/core/compiler/Compiler.php'); 68 | 69 | self::loadLib('lib/core/runtime/PrintPanel.php'); 70 | self::loadLib('lib/core/runtime/Runtime.php'); 71 | self::loadLib('lib/core/runtime/ModuleManager.php'); 72 | self::loadLib('lib/core/runtime/Security.php'); 73 | 74 | self::loadLib('lib/core/utils/GlobalFunctions.php'); 75 | self::loadLib('lib/core/utils/StringReader.php'); 76 | 77 | self::loadLib('lib/stdlib/classes/_index.php'); 78 | self::loadLib('lib/stdlib/modules/_index.php'); 79 | } 80 | 81 | static public function loadLib($file) { 82 | 83 | require_once(Tht::systemPath($file)); 84 | } 85 | 86 | } 87 | -------------------------------------------------------------------------------- /tht/lib/core/compiler/Symbol/Symbols/S_OpenParen.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | class S_OpenParen extends Symbol { 6 | 7 | var $bindingPower = 90; 8 | 9 | // Grouping (...) 10 | function asLeft($p) { 11 | 12 | $this->space('*(N'); 13 | 14 | $p->ignoreNewlines = true; 15 | 16 | $p->next(); 17 | $this->updateType(SymbolType::OPERATOR); 18 | 19 | $exp = $p->parseExpression(0); 20 | 21 | $p->ignoreNewlines = false; 22 | 23 | $p->now(')')->space('N)*'); 24 | $p->symbol->isOuterParen = $this->isOuterParen; 25 | $p->next(); 26 | 27 | return $exp; 28 | } 29 | 30 | // Function call. foo() 31 | function asInner($p, $left) { 32 | 33 | $this->space('x(N'); 34 | 35 | $sOpenParen = $p->symbol; 36 | 37 | $p->next(); 38 | $this->updateType(SymbolType::CALL); 39 | 40 | // Check for bare function like "print" 41 | if ($left->token[TOKEN_TYPE] === TokenType::WORD) { 42 | $type = u_Bare::isa($left->getValue()) ? SymbolType::BARE_FUN : SymbolType::USER_FUN; 43 | $left->updateType($type); 44 | if ($type === SymbolType::USER_FUN) { 45 | $p->registerUserFunction('called', $left->token); 46 | } 47 | } 48 | $this->addKids([ $left ]); 49 | 50 | // Argument list 51 | $args = []; 52 | $isMultiline = false; 53 | $pos = 0; 54 | while (true) { 55 | 56 | $isMultiline = $p->parseElementSeparator($pos, $isMultiline, ')'); 57 | $pos += 1; 58 | 59 | if ($p->symbol->isValue(')')) { 60 | break; 61 | } 62 | 63 | $arg = $p->parseExpression(0); 64 | 65 | if ($arg === null) { 66 | $p->error('Reached end of file without closing paren: `)`'); 67 | } 68 | if ($arg->type == SymbolType::BOOLEAN) { 69 | ErrorHandler::setHelpLink('/language-tour/option-maps', 'Option Maps'); 70 | $p->error('Can\'t use a Boolean as a function argument. Try: Use an option map instead. Ex: `{ flag: true }` or `-flag`', $arg->token); 71 | } 72 | $args[]= $arg; 73 | } 74 | 75 | if (!$p->symbol->isValue(')')) { 76 | $p->error('Expected closing paren `)`'); 77 | } 78 | 79 | if (!$args) { 80 | // empty parens: () 81 | $sOpenParen->space('x(x'); 82 | $p->space('x)*'); 83 | } 84 | else { 85 | $sOpenParen->space('x(N'); 86 | $p->space('N)*'); 87 | } 88 | 89 | $p->next(); 90 | 91 | $sArgs = $p->makeAstList(AstList::FLAT, $args); 92 | $this->addKid($sArgs); 93 | 94 | return $this; 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /tht/lib/core/main/Tht/ThtErrors.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | class ThtError extends \Exception { 6 | function u_message() { 7 | return $this->getMessage(); 8 | } 9 | } 10 | 11 | // Global functions for triggering errors. 12 | // Errors are handled in ErrorHandler. 13 | trait ThtErrors { 14 | 15 | static public function error($msg) { 16 | // Use custom exception to track extra caller info. 17 | throw new ThtError ($msg); 18 | } 19 | 20 | static public function configError($msg) { 21 | ErrorHandler::handleConfigError($msg); 22 | } 23 | 24 | // For startup errors, we assume nothing else is loaded and have to give a minimal error message. 25 | static public function startupError($msg) { 26 | ErrorHandlerMinimal::printError($msg); 27 | } 28 | 29 | static public function phpIniError($msg) { 30 | 31 | $iniPath = php_ini_loaded_file(); 32 | $msg .= " Try the following: // 1) Edit `$iniPath` // 2) Restart the web server"; 33 | 34 | self::startupError($msg); 35 | } 36 | 37 | static public function phpLibError($lib, $altHelpMsg='') { 38 | 39 | $iniPath = php_ini_loaded_file(); 40 | $msg = "PHP extension `$lib` must be installed and enabled. // Try the following: // 1) Edit `$iniPath` // 2) Remove the semicolon in front of this line: // `;extension=$lib` // 3) Restart the web server"; 41 | $msg .= ' // ' . $altHelpMsg; 42 | 43 | self::startupError($msg); 44 | } 45 | 46 | // Report error for the most recent userland function call 47 | static public function callerError($msg, $skipClass = '') { 48 | 49 | $callerFun = Tht::getUserlandCaller($skipClass)['function']; 50 | 51 | Tht::error("Function `$callerFun()` " . $msg); 52 | } 53 | 54 | static public function getUserlandCaller($skipClass = '') { 55 | 56 | $frames = debug_backtrace(DEBUG_BACKTRACE_IGNORE_ARGS); 57 | $callerFrame = false; 58 | 59 | foreach ($frames as $f) { 60 | if (hasu_($f['function'])) { 61 | if ($f['class'] !== 'o\\u_' . $skipClass) { 62 | $callerFrame = $f; 63 | break; 64 | } 65 | } 66 | } 67 | 68 | if (!$callerFrame) { 69 | $callerFrame = $frames[2]; // TODO: why? 70 | } 71 | 72 | $fun = $callerFrame['function']; 73 | $fun = ModuleManager::cleanNamespacedFunction($fun); 74 | 75 | // TODO: ugly. This should be consolidated somewhere 76 | $class = $callerFrame['class'] ?? ''; 77 | $class = str_replace('o\\', '', $class); 78 | 79 | // if ($class) { $class .= '.'; } 80 | 81 | return [ 82 | 'class' => $class, 83 | 'function' => $fun, 84 | 'file' => $callerFrame['file'], 85 | ]; 86 | } 87 | 88 | } 89 | -------------------------------------------------------------------------------- /tests/code/modules/tests/lang/LoopTests.tht: -------------------------------------------------------------------------------- 1 | 2 | public fun run($t) { 3 | 4 | $t.section('Loops') 5 | 6 | $s = '' 7 | foreach range(1, 3) as $i { 8 | $s ~= $i 9 | } 10 | $t.ok($s == '123', 'foreach, range') 11 | 12 | $s = '' 13 | foreach range(5, 1) as $i { 14 | $s ~= $i 15 | } 16 | $t.ok($s == '54321', 'desc range') 17 | 18 | $s = '' 19 | foreach range(0, 8, 2) as $i { 20 | $s ~= $i 21 | } 22 | $t.ok($s == '02468', 'range step 2') 23 | 24 | 25 | $s = '' 26 | $nums = [1, 2, 3, 4, 5] 27 | foreach $nums as $n { 28 | $s ~= $n 29 | } 30 | $t.ok($s == '12345', 'foreach, list') 31 | $s = '' 32 | foreach $nums as $n { 33 | $s ~= $n 34 | } 35 | $t.ok($s == '12345', 'foreach, list - second time (reset)') 36 | 37 | 38 | 39 | $pairs = { a: 1, b: 2, c: 3 } 40 | $s = '' 41 | foreach $pairs as $letter/$number { 42 | $s ~= $number ~ $letter 43 | } 44 | $t.ok($s == '1a2b3c', 'foreach, map') 45 | 46 | $s = '' 47 | foreach $pairs as $letter/$number { 48 | $s ~= $number ~ $letter 49 | } 50 | $t.ok($s == '1a2b3c', 'foreach, map - second time (reset)') 51 | 52 | 53 | $s = '' 54 | foreach range(7, 9) as $i/$num { 55 | $s ~= $i ~ $num 56 | } 57 | $t.ok($s == '172839', 'foreach, range with index') 58 | 59 | $s = '' 60 | foreach [4, 5, 6] as $i/$num { 61 | $s ~= $i ~ $num 62 | } 63 | $t.ok($s == '142536', 'foreach, list with index') 64 | 65 | 66 | 67 | 68 | $i = 0 69 | $s = '' 70 | loop { 71 | $i += 1 72 | $s ~= $i 73 | if $i == 3: break 74 | } 75 | $t.ok($s == '123', 'loop - break') 76 | 77 | $i = 0 78 | $s = '' 79 | loop { 80 | $i += 1 81 | if $i == 4: continue 82 | $s ~= $i 83 | if $i == 5: break 84 | } 85 | $t.ok($s == '1235', 'loop - continue') 86 | 87 | 88 | $i = 0 89 | foreach [11, 22, 33] as $n { 90 | $i = $n 91 | if $n > 20: break 92 | } 93 | $t.ok($i == 22, 'foreach - break') 94 | 95 | $i = 0 96 | foreach [11, 22, 33] as $n { 97 | $i = $n 98 | if $n > 20: continue 99 | } 100 | $t.ok($i == 33, 'foreach - continue') 101 | 102 | 103 | $t.parserError('loop {\n $a = 1\n}\n', 'needs a \'break\'') 104 | $t.parserError('loop {\n loop { break }\n}\n', 'needs a \'break\'') 105 | $t.parserOk('loop {\n loop { break }\n break\n}\n', 'nested breaks') 106 | $t.parserOk('fun go {\n loop {\n return\n }\n}', 'return instead of break') 107 | 108 | $t.parserError('$a = 1\nbreak', 'not allowed outside of a loop: `break`') 109 | $t.parserError('$a = 1\ncontinue', 'not allowed outside of a loop: `continue`') 110 | 111 | return $t 112 | } 113 | 114 | -------------------------------------------------------------------------------- /tht/lib/stdlib/modules/Input/InputValidatorRuleParser.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | trait InputValidatorRuleParser { 6 | 7 | public function getRuleMapForString($paramName, $rulesetString) { 8 | 9 | if ($rulesetString == '') { 10 | return $this->defaultRuleMap; 11 | } 12 | 13 | $ruleMap = $this->parseRulesetString($rulesetString); 14 | $this->checkRuleMapNames($paramName, $ruleMap); 15 | $mergedRuleMap = $this->mergeRuleMap($ruleMap); 16 | 17 | return $mergedRuleMap; 18 | } 19 | 20 | function checkRuleMapNames($paramName, $ruleMap) { 21 | 22 | if (!isset($ruleMap['type'])) { 23 | $this->error("Missing validation type for input param: `$paramName`"); 24 | } 25 | 26 | $typeRule = $ruleMap['type']; 27 | if (!isset($this->typeToRuleMap[$typeRule])) { 28 | $this->error("Unknown validation type `$typeRule` for input param: `$paramName`"); 29 | } 30 | 31 | foreach ($ruleMap as $ruleName => $ruleValue) { 32 | 33 | if ($ruleName == 'type') { continue; } 34 | 35 | if (!array_key_exists($ruleName, $this->defaultRuleMap)) { 36 | $this->error("Unknown validation rule `$ruleName` for input param: `$paramName`"); 37 | } 38 | 39 | if ($ruleName != 'type' && isset($this->typeToRuleMap[$ruleName])) { 40 | $this->error("Extra validation type `$ruleName` for input param: `$paramName`"); 41 | } 42 | } 43 | } 44 | 45 | // Merge higher-level rules into the default rulemap (overriding the default) 46 | function mergeRuleMap($ruleMap) { 47 | 48 | // Merge the `type` rulemap into the default rulemap 49 | $mergedRuleMap = array_merge( 50 | $this->defaultRuleMap, 51 | $this->typeToRuleMap[$ruleMap['type']] 52 | ); 53 | 54 | // Merge in individual rules 55 | foreach ($ruleMap as $ruleName => $ruleValue) { 56 | if ($ruleName != 'type') { 57 | $mergedRuleMap[$ruleName] = $ruleValue; 58 | } 59 | } 60 | 61 | return $mergedRuleMap; 62 | } 63 | 64 | // Convert 'foo|bar:123' ---> ['type' => 'foo', 'bar' => 123] 65 | function parseRulesetString($sRuleset) { 66 | 67 | if (OMap::isa($sRuleset)) { 68 | return $sRuleset; 69 | } 70 | 71 | $rules = explode('|', $sRuleset); 72 | 73 | $typeRule = array_shift($rules); 74 | $rulesetMap = [ 75 | 'type' => $typeRule 76 | ]; 77 | 78 | foreach ($rules as $r) { 79 | $parts = explode(':', $r, 2); 80 | if (count($parts) == 1) { 81 | $rulesetMap[$parts[0]] = true; 82 | } 83 | else { 84 | $rulesetMap[$parts[0]] = $parts[1]; 85 | } 86 | } 87 | 88 | return $rulesetMap; 89 | } 90 | 91 | } -------------------------------------------------------------------------------- /tht/lib/core/compiler/Symbol/Symbols/S_OpenCurly.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | class S_OpenCurly extends Symbol { 6 | 7 | var $type = SymbolType::AST_LIST; 8 | public $value = ''; 9 | 10 | // Map Literal { ... } 11 | function asLeft($p) { 12 | 13 | $sOpenBrace = $p->symbol; 14 | $p->next(); 15 | 16 | $pairs = []; 17 | $hasKey = []; 18 | 19 | if ($p->symbol->isValue("}")) { 20 | $this->space('*{N'); 21 | } 22 | else { 23 | $this->space('*{ '); 24 | } 25 | 26 | // Collect "key: value" pairs 27 | $isMultiline = false; 28 | $pos = 0; 29 | while (true) { 30 | 31 | $isMultiline = $p->parseElementSeparator($pos, $isMultiline, '}'); 32 | $pos += 1; 33 | 34 | if ($p->symbol->isValue("}")) { 35 | break; 36 | } 37 | 38 | // key 39 | $key = $p->symbol; 40 | $strKey = $key->getValue(); 41 | 42 | if (isset($hasKey[$strKey])) { 43 | $p->error("Duplicate key: `$strKey`"); 44 | } 45 | else if ($key->type == SymbolType::USER_VAR) { 46 | $p->error("Variable not allowed as Map key."); 47 | } 48 | else if (!$key->allowAsMapKey) { 49 | $p->error("Map key must be a string, number, or word.", $key->token); 50 | } 51 | 52 | 53 | $key->updateType(SymbolType::MAP_KEY); 54 | $hasKey[$strKey] = true; 55 | $p->next(); 56 | 57 | $sVal = null; 58 | 59 | if ($p->symbol->isValue(':')) { 60 | // explicit value 61 | $p->now(':', 'map.colon')->space('x:S')->next(); 62 | $sVal = $p->parseExpression(0); 63 | } 64 | else { 65 | // implicit value: e.g. { foo } -> { foo: 'foo' } 66 | $word = $key->getValue(); 67 | if (!preg_match('/^[a-zA-Z0-9]+$/', $word)) { 68 | $p->error("Invalid token in Map: `$word` Try: Add quotes.", $key->token); 69 | } 70 | $sVal = $p->makeSymbol(SymbolType::STRING, $key->getValue(), SymbolType::STRING); 71 | } 72 | 73 | $pair = $p->makeSymbol(SymbolType::MAP_PAIR, $key->getValue(), SymbolType::MAP_PAIR); 74 | $pair->addKid($sVal); 75 | 76 | $pairs []= $pair; 77 | } 78 | 79 | if (count($pairs) == 0) { 80 | // Single line should have no inner padding: {} 81 | $p->space($isMultiline ? 'B}*' : 'x}*'); 82 | } 83 | else { 84 | $sOpenBrace->space($isMultiline ? '*{B' : '*{S'); 85 | $p->space($isMultiline ? 'B}*' : 'S}*'); 86 | } 87 | 88 | $p->next(); 89 | 90 | $this->addKids($pairs); 91 | $this->value = AstList::MAP; 92 | 93 | return $this; 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /tht/lib/stdlib/classes/ORegex.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | class ORegex extends OVar { 6 | 7 | protected $type = 'regex'; 8 | static public $ALLOWED_FLAGS = 'msix'; 9 | 10 | private $pattern = ''; 11 | private $flags = ''; 12 | private $startIndex = ONE_INDEX; 13 | 14 | function __toString() { 15 | return $this->toObjectString(); 16 | } 17 | 18 | public function jsonSerialize():mixed { 19 | return $this->toObjectString(); 20 | } 21 | 22 | public function u_to_string() { 23 | return $this->toObjectString(); 24 | } 25 | 26 | function toObjectString() { 27 | $summary = $this->getPattern(); 28 | return self::getObjectString($this->bareClassName(), $summary); 29 | } 30 | 31 | 32 | public function u_on_print() { 33 | 34 | $this->ARGS('', func_get_args()); 35 | 36 | return $this->toObjectString(); 37 | } 38 | 39 | function __construct($pat, $flags='') { 40 | 41 | $this->pattern = $pat; 42 | $this->flags = $this->validateFlags($flags); 43 | } 44 | 45 | function getPattern() { 46 | 47 | $pat = $this->getRawPattern(); 48 | 49 | // Always implicitly add u = unicode 50 | return '/' . $pat . '/u' . $this->flags; 51 | } 52 | 53 | function getRawPattern() { 54 | 55 | $pat = str_replace('/', '\\/', $this->pattern); 56 | return $pat; 57 | } 58 | 59 | function setPattern($pat) { 60 | 61 | $this->pattern = $pat; 62 | } 63 | 64 | function u_flags($f) { 65 | 66 | $this->ARGS('s', func_get_args()); 67 | $this->flags = $this->validateFlags($f); 68 | 69 | return $this; 70 | } 71 | 72 | function u_start_index($startIndex) { 73 | 74 | $this->ARGS('i', func_get_args()); 75 | $this->startIndex = $startIndex; 76 | 77 | return $this; 78 | } 79 | 80 | function getStartIndex() { 81 | 82 | return $this->startIndex; 83 | } 84 | 85 | function addFlag($flag) { 86 | 87 | if (strpos($this->flags, $flag) === false) { 88 | $this->flags .= $flag; 89 | } 90 | } 91 | 92 | function validateFlags($flags) { 93 | 94 | $flags = trim($flags); 95 | if (!$flags) { return ''; } 96 | 97 | foreach (str_split($flags) as $f) { 98 | if (strpos(self::$ALLOWED_FLAGS, $f) === false) { 99 | $this->error("Invalid Regex flag: `$f` Try: `m s i x`"); 100 | } 101 | } 102 | 103 | return $flags; 104 | } 105 | } 106 | 107 | class u_Regex extends OClass { 108 | 109 | function newObject($className, $args) { 110 | 111 | if (!isset($args[1])) { 112 | $args[1] = ''; 113 | } 114 | $this->ARGS('sS', $args); 115 | 116 | return new ORegex ($args[0], $args[1]); 117 | } 118 | } 119 | 120 | -------------------------------------------------------------------------------- /tests/code/modules/tests/lang/UserModuleTests.tht: -------------------------------------------------------------------------------- 1 | 2 | public fun run($t) { 3 | 4 | $t.section('User Modules') 5 | 6 | if $t.skipSlowTests(): return $t 7 | 8 | $t.ok(TestModule.bareFun('Joe') == 'bareFunction:Joe', 'module call - autoloaded') 9 | 10 | $t.ok(TestModule.testModuleVar() == 'moduleVar:mod', 'module var - inside access') 11 | $t.ok(TestModule.ModuleConstant == 'constant', 'module constant - outside access') 12 | 13 | $t.dies( 14 | fun { $a = TestModule.moduleVar } 15 | 'Can`t read private module variable' 16 | ) 17 | 18 | $t.dies( 19 | fun { TestModule.ModuleConstant = 'outsideChange' } 20 | 'Can`t set field' 21 | ) 22 | $t.dies( 23 | fun { TestModule.changeConstantAndDie() } 24 | 'Can`t write to read-only constant' 25 | ) 26 | 27 | $t.dies( 28 | fun { TestModule.nonExportedFn() } 29 | 'Can`t call non-public' 30 | ) 31 | 32 | $t.dies( 33 | fun { $m = load('Math') } 34 | 'already exists as a standard module' 35 | ) 36 | 37 | // Meta Methods 38 | 39 | $t.ok(TestModule.type() == 'module', 'type') 40 | $t.ok(TestModule.zClassName() == 'TestModule', 'zClassName') 41 | $t.ok(TestModule.zHasField('moduleVar'), 'zHasField') 42 | $t.ok(TestModule.zHasField('moduleVar'), 'zHasField') 43 | $t.ok(!TestModule.zHasField('notExist'), 'zHasField - false') 44 | $t.ok(TestModule.zHasMethod('bareFun'), 'zHasMethod = true') 45 | $t.ok(!TestModule.zHasMethod('notExist'), 'zHasMethod = false') 46 | $t.ok(TestModule.zGetMethods().length() == 3, 'zGetMethods') 47 | $t.ok(TestModule.zGetFields().length() == 3, 'zGetFields') 48 | $t.ok(TestModule.zGetField('moduleVar') == 'mod', 'zGetField') 49 | $t.dies(fun { TestModule.zSetField('moduleVar', 3) }, 'read-only') 50 | 51 | 52 | 53 | load('subDir/OtherModule') 54 | $t.ok(OtherModule.ok('Foo') == 'ok:Foo', 'import from subfolder') 55 | 56 | $t 57 | .dies(fun { load('http://tht.dev') }, 'import url', 'Invalid character in `load` path') 58 | .dies(fun { load('../Foo') }, 'Source file not found') 59 | .dies(fun { load('Foo%') }, 'import with illegal char', 'Invalid character in `load` path') 60 | .dies(fun { load('Foo.tht') }, 'import with tht extension', 'Please remove `.tht`') 61 | 62 | //$t.ok(BaseObject.BaseModuleConstant == 'constant', 'constant from subfolder module') 63 | $t.dies( 64 | fun { TestModule.ConstantMap.Purple = 'xxx' } 65 | 'can`t modify constant' 66 | 'Can`t modify read-only' 67 | ) 68 | 69 | // load modules with relative paths 70 | $t.ok(OtherModule.useAdjacent() == 'adjacent', 'call rel adjacent module') 71 | load('subDir/AdjacentModule') 72 | $t.ok(AdjacentModule.callUpperModule() == 'bareFunction:adj', 'call rel parent module') 73 | 74 | $t.dies(fun { load('subDir/Adjacentmodule') }, 'file name mismatch', 'Check exact spelling') 75 | 76 | return $t 77 | } 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /tht/lib/stdlib/classes/OTypeString/OCore.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | // For url'...' and file'...', see OUrl and OFile 6 | 7 | class JconTypeString extends OTypeString { 8 | 9 | protected $stringType = 'jcon'; 10 | } 11 | 12 | class HtmlTypeString extends OTypeString { 13 | 14 | protected $stringType = 'html'; 15 | 16 | protected function u_z_escape_param($v) { 17 | return Security::escapeHtml($v); 18 | } 19 | // TODO: Security - review how this compares to security in HtmlTemplateTransformer (inTag context) 20 | // function u_fill($params) { 21 | // $this->error('(Security) HtmlTypeString with placeholders not supported. Try: `-Html` template or `Web.link`'); 22 | // } 23 | } 24 | 25 | class JsTypeString extends OTypeString { 26 | 27 | protected $stringType = 'js'; 28 | 29 | protected function u_z_escape_param($v) { 30 | 31 | if (is_bool($v)) { 32 | return $v ? 'true' : 'false'; 33 | } 34 | else if (is_object($v)) { 35 | return json_encode($v->val); 36 | } 37 | else if (is_array($v)) { 38 | return json_encode($v); 39 | } 40 | else if (vIsNumber($v)) { 41 | return $v; 42 | } 43 | else { 44 | $v = '' . $v; 45 | $v = str_replace('"', '\\"', $v); 46 | $v = str_replace("\n", '\\n', $v); 47 | return "\"$v\""; 48 | } 49 | } 50 | } 51 | 52 | class CssTypeString extends OTypeString { 53 | 54 | protected $stringType = 'css'; 55 | 56 | protected function u_z_escape_param($v) { 57 | return Tht::module('Output')->escapeCss($v); 58 | } 59 | } 60 | 61 | class SqlTypeString extends OTypeString { 62 | 63 | protected $stringType = 'sql'; 64 | 65 | protected function u_z_escape_param($v) { 66 | $this->error('SQL escaping must be handled internally.'); 67 | } 68 | 69 | // This is only used for debugging. We rely on PDO to do escaping, etc. 70 | function u_render_string() { 71 | return $this->str; 72 | } 73 | } 74 | 75 | class CmdTypeString extends OTypeString { 76 | 77 | protected $stringType = 'cmd'; 78 | 79 | protected function u_z_escape_param($v) { 80 | return escapeshellarg($v); 81 | } 82 | } 83 | 84 | class PlainTypeString extends OTypeString { 85 | 86 | protected $stringType = 'plain'; 87 | 88 | protected function u_z_escape_param($k) { 89 | return $k; 90 | } 91 | } 92 | 93 | class LmTypeString extends OTypeString { 94 | 95 | protected $stringType = 'lm'; 96 | 97 | protected function u_z_escape_param($k) { 98 | return $k; 99 | } 100 | } 101 | 102 | class JsonTypeString extends OTypeString { 103 | 104 | protected $stringType = 'json'; 105 | 106 | protected function u_z_escape_param($k) { 107 | $this->error('JSON params should be added to the data before being converted to a String.'); 108 | } 109 | } 110 | 111 | -------------------------------------------------------------------------------- /tht/lib/core/data/starterApp/code/pages/examples/database.tht: -------------------------------------------------------------------------------- 1 | 2 | fun main { 3 | 4 | $products = getAllFromCart() 5 | 6 | $page = Page.create({ 7 | title: 'Database Example' 8 | css: url'/vendor/basic.css' 9 | main: pageHtml($products) 10 | }) 11 | 12 | Output.sendPage($page) 13 | } 14 | 15 | 16 | // Modes (via 'mode' param in links) 17 | // See: https://tht.dev/reference/page-modes 18 | //---------------------------------------------------------- 19 | 20 | fun addTacosMode { 21 | 22 | addToCart('tacos', Math.random(1, 4)) 23 | 24 | return true // refresh page 25 | } 26 | 27 | fun addTatersMode { 28 | 29 | addToCart('taters', Math.random(1, 6)) 30 | 31 | return true 32 | } 33 | 34 | fun deleteAllTacosMode { 35 | 36 | deleteFromCart('tacos') 37 | 38 | return true 39 | } 40 | 41 | fun deleteAllTatersMode { 42 | 43 | deleteFromCart('taters') 44 | 45 | return true 46 | } 47 | 48 | 49 | // Database Access 50 | //---------------------------------------------------------- 51 | 52 | // Database: data/db/app.db (sqlite) 53 | // Table: cart 54 | // Columns: 55 | // product: varchar 56 | // quantity: int 57 | 58 | fun getAllFromCart { 59 | 60 | return Db.selectRows(sql'select * from cart') 61 | } 62 | 63 | fun addToCart($product, $num) { 64 | 65 | Db.insertRow('cart', { product: $product, quantity: $num }) 66 | } 67 | 68 | fun deleteFromCart($product) { 69 | 70 | Db.deleteRows('cart', { product: $product }) 71 | } 72 | 73 | 74 | // Templates 75 | //---------------------------------------------------------- 76 | 77 | tem pageHtml($products) { 78 | 79 | <h1> Database Example 80 | 81 | <table> 82 | <tr> 83 | <th> Product 84 | <th> Quantity 85 | </> 86 | --- foreach $products as $p { 87 | <tr> 88 | <td> {{ $p.product }} 89 | <td> {{ $p.quantity }} 90 | </> 91 | --- } 92 | </> 93 | 94 | --- if !$products { 95 | <p> <i> No items in cart. 96 | --- } 97 | 98 | 99 | {{ linksHtml() }} 100 | } 101 | 102 | tem linksHtml { 103 | 104 | <style> 105 | .links { margin-top: 6rem; width: 42rem; } 106 | .links button { margin: 1.5rem 0; width: 20rem; } 107 | </> 108 | 109 | <.links> 110 | {{ button('addTacos', 'plus') }} 111 | {{ button('deleteAllTacos', 'cancel') }} 112 | {{ button('addTaters', 'plus') }} 113 | {{ button('deleteAllTaters', 'cancel') }} 114 | </> 115 | } 116 | 117 | // Creates a button that submits data via a hidden form, 118 | // wthout the need for AJAX. 119 | fun button($mode, $iconType) { 120 | 121 | $data = { mode: $mode } 122 | $label = $mode.toHumanized().toTitleCase() 123 | $iconLabel = iconLabelHtml($label, $iconType) 124 | 125 | return Web.postLink($iconLabel, url'this', $data) 126 | } 127 | 128 | tem iconLabelHtml($label, $iconType) { 129 | {{ Web.icon($iconType) }} {{ $label }} 130 | } 131 | 132 | 133 | -------------------------------------------------------------------------------- /tests/code/pages/errors/-todo.tht: -------------------------------------------------------------------------------- 1 | 2 | 3 | // Split these to separate files 4 | 5 | 6 | 7 | // Std Modules - not enough arguments (triggered by PHP) 8 | // 9 | 10 | // Built-in arg type 11 | // doSomething({ foo: 'bar' }) 12 | // fn doSomething($msg:s) { 13 | // print($msg) 14 | // } 15 | 16 | 17 | // With stack trace 18 | // TODO: fix formatting of function names 19 | 20 | // Standalone expression/literals 21 | //$a == 123 22 | //$a 23 | //true 24 | //{} 25 | 26 | 27 | // Unknown function (tht validator) 28 | // myFun() 29 | 30 | // wrong function case 31 | // myFuN() 32 | // fn myFun() { } 33 | 34 | // Unknown function (passes validation, but caught in PHP) 35 | // bar() 36 | // fn foo() { 37 | // fn bar($a) { 38 | // print('bar' ~ $a) 39 | // } 40 | // } 41 | 42 | 43 | // Non-existent field, in nested calls. 44 | // doSomething() 45 | // fn doSomething() { 46 | // doAnotherThing() 47 | // } 48 | // fn doAnotherThing() { 49 | // $a = {} 50 | // $b = $a.foo 51 | // } 52 | 53 | 54 | // undefined function 55 | //nope() 56 | 57 | 58 | 59 | // PHP Runtime Errors 60 | //------------------------------- 61 | 62 | // divide by zero 63 | //if 2 / 0: print('x') 64 | 65 | 66 | 67 | // Memory 68 | // $s = ''; 69 | // foreach range(1, 10000) as $x { 70 | // foreach range(1, 10000) as $y { 71 | // $s ~= 'xxxxxxxxxx'; 72 | // } 73 | // } 74 | 75 | 76 | 77 | 78 | // THT Runtime Errors 79 | //------------------------------- 80 | 81 | // standard arguments (too many) 82 | //Math.floor(1, 1); 83 | 84 | //print('foo'.length('x')); 85 | 86 | // non-number add 87 | //$a = 'cat' + 3 88 | 89 | // argument type 90 | // add('cat'); 91 | // F add($n:i) { } 92 | 93 | // List 94 | // $l = []; 95 | // print($l.first(3)); 96 | 97 | // require typestring 98 | //Response.redirect('/'); 99 | 100 | // stdmodule error 101 | //Json.decode('sdfsdf'); 102 | 103 | // user triggered 104 | //die('foo'); 105 | 106 | // map via bag 107 | // $m = {}; 108 | // $m.foo = 123; 109 | 110 | // security 111 | //Php.call('eval', ''); 112 | 113 | // scope - in function 114 | // TODO - catch at compile time, write test 115 | // foo(1); 116 | // function foo($z) { 117 | 118 | // inner(); 119 | // function inner() { 120 | // print('hi'); 121 | // } 122 | // } 123 | 124 | 125 | 126 | // PHP Shutdown Errors 127 | //------------------------------- 128 | 129 | // too few arguments 130 | // TODO: message formatting 131 | // go(123); 132 | // function go($foo:s) { } 133 | 134 | //Response.sendHtml(scriptHtml()); 135 | 136 | 137 | // tm scriptHtml { 138 | // Hello 139 | // <script foo='bar'> 140 | // console.log('test'); 141 | // </> 142 | // } 143 | 144 | 145 | 146 | // Resource Errors 147 | //------------------------------- 148 | 149 | // Exceeded memory 150 | // $a = [] 151 | // loop { 152 | // $a.push('xxx') 153 | // if false: break 154 | // } 155 | 156 | 157 | 158 | 159 | 160 | // $abc = 'abc' 161 | // $abc.contains('') 162 | 163 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /tht/lib/stdlib/classes/OStdModule.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | // TODO: factor out commonalities with OModule & OClass 6 | 7 | class OStdModule extends OClass implements \JsonSerializable { 8 | 9 | function u_type() { 10 | return 'module'; 11 | } 12 | 13 | function bareClassName() { 14 | return $this->getClass(); 15 | } 16 | 17 | function getClass() { 18 | 19 | $c = str_replace('o\\u_', '', get_called_class()); 20 | return $c; 21 | } 22 | 23 | function error($msg) { 24 | 25 | ErrorHandler::addOrigin('stdModule.' . strtolower($this->getClass())); 26 | 27 | Tht::error($msg); 28 | } 29 | 30 | function addErrorHelpLink($method = '') { 31 | ErrorHandler::setStdLibHelpLink('module', $this->getClass(), $method); 32 | } 33 | 34 | // TODO: duplicated with OClass 35 | function argumentError($msg, $method) { 36 | 37 | $methodToken = v($method)->u_to_token_case('-'); 38 | $methodLabel = $method; 39 | 40 | $label = $this->getClass() . '.' . $methodLabel; 41 | 42 | ErrorHandler::setHelpLink('/manual/module/' . strtolower($this->getClass()) . '/' . $methodToken, $label); 43 | ErrorHandler::addOrigin('stdModule.' . strtolower($this->getClass())); 44 | 45 | Tht::error($msg); 46 | } 47 | 48 | function ARGS($sig, $args) { 49 | 50 | $err = validateFunctionArgs($sig, $args); 51 | 52 | if ($err) { 53 | $this->argumentError($err['msg'], unu_($err['function'])); 54 | } 55 | } 56 | 57 | function __set($k, $v) { 58 | Tht::error("Can't set field on a standard module: `$k`"); 59 | } 60 | 61 | function __get($f) { 62 | $f = unu_($f); 63 | Tht::error("Can't get field on a standard module: `$f` Try: Call method `$f()`"); 64 | } 65 | 66 | function toObjectString() { 67 | return OClass::getObjectString( 68 | $this->cleanPackageName(get_called_class()) . ' Module' 69 | ); 70 | } 71 | 72 | // TODO: some overlap with OClass 73 | function __call($method, $args) { 74 | 75 | $method = unu_($method); 76 | 77 | $suggest = $this->getSuggestedMethod($method); 78 | $c = $this->getClass(); 79 | 80 | if (!$suggest) { 81 | 82 | // Look if method is in other Std Modules 83 | $possibles = []; 84 | foreach (StdLibModules::$files as $lib) { 85 | if (method_exists(Tht::module($lib), u_($method))) { 86 | $possibles []= '`' . $lib . '.' . $method . '()`'; 87 | } 88 | } 89 | 90 | if (count($possibles)) { 91 | $suggest = 'Try: ' . implode(', ', $possibles); 92 | } 93 | } 94 | else { 95 | // Insert module name 96 | $suggest = preg_replace('/`(\w+)/', '`' . $c . '.$1', $suggest); 97 | } 98 | 99 | ErrorHandler::setHelpLink('/manual/module/' . strtolower($c), $c); 100 | $this->error("Unknown module method: `$c.$method()` $suggest"); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /tht/lib/stdlib/modules/String.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | class u_String extends OStdModule { 6 | 7 | public $lastReplaceCount = -1; 8 | 9 | function u_unicode_to_char($num) { 10 | 11 | $this->ARGS('n', func_get_args()); 12 | 13 | return iconv('UCS-4LE', 'UTF-8', pack('V', $num)); 14 | } 15 | 16 | // function u_range($from, $to) { 17 | // $this->ARGS('ss', func_get_args()); 18 | // return OList::create(range($from, $to)); 19 | // } 20 | 21 | // Undocumented 22 | function u_x_danger_password($plaintext) { 23 | 24 | $this->ARGS('s', func_get_args()); 25 | 26 | return Security::createPassword($plaintext); 27 | } 28 | 29 | function u_random_token($len) { 30 | 31 | $this->ARGS('n', func_get_args()); 32 | 33 | $s = Security::randomString($len); 34 | 35 | return $s; 36 | } 37 | 38 | function u_scramble_id($idToScramble) { 39 | $this->ARGS('n', func_get_args()); 40 | return Security::encodeHashId($idToScramble); 41 | } 42 | 43 | function u_unscramble_id($scrambledId) { 44 | $this->ARGS('s', func_get_args()); 45 | return Security::decodeHashId($scrambledId); 46 | } 47 | 48 | function u_unique_id() { 49 | 50 | $this->ARGS('', func_get_args()); 51 | 52 | return Security::createUuid(); 53 | } 54 | 55 | function u_char_list($listId) { 56 | 57 | $this->ARGS('s', func_get_args()); 58 | 59 | $s = ''; 60 | switch ($listId) { 61 | case 'all': $s = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; break; 62 | case 'letters': $s = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'; break; 63 | case 'lettersLower': $s = 'abcdefghijklmnopqrstuvwxyz'; break; 64 | case 'lettersUpper': $s = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; break; 65 | case 'digits': $s = '0123456789'; break; 66 | case 'hex': $s = '0123456789abcdefABCDEF'; break; 67 | case 'hexUpper': $s = '0123456789ABCDEF'; break; 68 | case 'hexLower': $s = '0123456789abcdef'; break; 69 | case 'octal': $s = '01234567'; break; 70 | } 71 | 72 | if (!$s) { 73 | $this->error('Unknown charList: `' . $listId . '`'); 74 | } 75 | 76 | return OList::create(str_split($s)); 77 | } 78 | 79 | function u_from_bytes($bytes) { 80 | 81 | $this->ARGS('l', func_get_args()); 82 | 83 | foreach ($bytes as $b) { 84 | if (!is_int($b) || $b < 0) { 85 | $this->error('Argument #1 must be a list of positive integers.'); 86 | } 87 | } 88 | 89 | $str = pack('C*', ...$bytes); 90 | 91 | return $str; 92 | } 93 | 94 | function u_last_replace_count() { 95 | 96 | $this->ARGS('', func_get_args()); 97 | 98 | if ($this->lastReplaceCount < 0) { 99 | $this->error('No replacement method has been called yet.'); 100 | } 101 | 102 | return $this->lastReplaceCount; 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /tests/code/modules/tests/errors/RuntimeErrorTests.tht: -------------------------------------------------------------------------------- 1 | 2 | public fun run($t) { 3 | 4 | $t.section('Runtime Errors') 5 | 6 | $t.dies(x{ 'abc'.sdf() }, 'Unknown object method') 7 | $t.dies(x{ { a: 1 }.sdfsdf() }, 'Unknown object method') 8 | 9 | $funFor = fun { 10 | foreach 2 as $foo { 11 | } 12 | } 13 | $t.dies($funFor, 'bad foreach var', 'argument') 14 | 15 | $str = 'abc' 16 | $map = { someKey: 123 } 17 | $list = [1, 2, 3] 18 | 19 | 20 | $t.section('Suggested Methods/Fields - String') 21 | 22 | .dies(fun { $a = $str.lowerCase }, 'toLowerCase()') 23 | .dies(fun { $a = $str.explode }, 'split(delimiter)') 24 | .dies(fun { $a = $str.len }, 'length()') 25 | .dies(fun { $a = $str.len() }, 'length()') 26 | .dies(fun { $a = $str.spilt }, 'split()') 27 | .dies(fun { $a = $str.split }, 'split()') 28 | .dies(fun { $a = $str.spilt() }, 'split()') 29 | .dies(fun { $a = $str.char }, 'getChar()') 30 | 31 | .dies(x{ 'x'.splitwords() }, 'Try: `splitWords()`') 32 | .dies(x{ (1).leftZeroPad() }, 'Try: `zeroPadLeft()`') 33 | .dies(x{ (1).even() }, 'Try: `isEven()`') 34 | .dies(x{ 'x'.leftPad() }, 'Try: `padLeft()`') 35 | .dies(x{ 'x'.pad() }, 'Try: `padRight()` `padLeft()` `padBoth()`') 36 | .dies(x{ 'x'.spilt() }, 'Try: `split()`') 37 | .dies(x{ 'x'.starts() }, 'Try: `startsWith()`') 38 | .dies(x{ 'x'.toType() }, 'Try: `xDangerToType()`') 39 | 40 | 41 | $t.section('Suggested Methods/Fields - Map') 42 | 43 | .dies(fun { $a = $map.keys }, 'keys()') 44 | .dies(fun { $a = $map.somekey }, 'someKey') 45 | .dies(fun { $a = $map.keySome }, 'someKey') 46 | .dies(fun { $a = $map.someKye }, 'someKey') 47 | .dies(fun { $a = $map.somKey }, 'someKey') 48 | .dies(fun { $a = $map.some }, 'someKey') 49 | .dies(fun { $a = $map.delete() }, 'remove') 50 | 51 | $t.section('Suggested Methods/Fields - List') 52 | 53 | .dies(fun { $a = $list.shift() }, 'popFirst()') 54 | .dies(fun { $a = $list.empty() }, 'isEmpty()') 55 | .dies(fun { $a = $list.len() }, 'length()') 56 | .dies(fun { $a = $list.length }, 'length()') 57 | .dies(fun { $a = $list.count }, 'length()') 58 | 59 | 60 | $t.section('Suggested Methods/Fields - Std Module') 61 | 62 | .dies(fun { $a = Json.stringify() }, 'Json.encode()') 63 | .dies(fun { $a = Output.json() }, 'Output.sendJson()') 64 | .dies(fun { $a = Output.header() }, 'Output.setHeader()') 65 | .dies(fun { $a = Db.select() }, 'Try: `Db.selectRow()` `Db.selectRows()`') 66 | .dies(fun { $a = Date.locale() }, 'Date.setLocale()') 67 | .dies(x{ Math.random }, 'Try: Call method `random()`') 68 | .dies(x{ Math.radnom() }, 'Try: `Math.random()`') 69 | .dies(x{ Page.random() }, 'Try: `Math.random()`') 70 | 71 | $t.section('Suggested Methods/Fields - User Module') 72 | 73 | 74 | load('subDir/OtherModule') 75 | $t.dies(x{ OtherModule.useAdjacant() }, 'Try: `useAdjacent()`') 76 | $t.dies(x{ OtherModule.useAdjacent }, 'Try: Call method `useAdjacent()`') 77 | 78 | 79 | 80 | return $t 81 | } 82 | 83 | 84 | 85 | -------------------------------------------------------------------------------- /tht/lib/core/compiler/Symbol/Symbols/S_Match.php: -------------------------------------------------------------------------------- 1 | <?php 2 | 3 | namespace o; 4 | 5 | class S_Match extends S_Statement { 6 | 7 | var $type = SymbolType::OPERATOR; 8 | var $allowAsMapKey = true; 9 | 10 | function asLeft($p) { 11 | 12 | $s = $this->asStatement($p); 13 | 14 | return $s; 15 | } 16 | 17 | // match { ... } 18 | function asStatement($p) { 19 | 20 | $this->space('*matchS'); 21 | 22 | $p->next(); 23 | 24 | $p->expressionDepth += 1; // prevent assignment 25 | 26 | if (!$p->symbol->isValue('{')) { 27 | $sMatchSubject = $p->parseExpression(0, 'noOuterParen'); 28 | if ($sMatchSubject->type == SymbolType::BOOLEAN) { 29 | $bool = $sMatchSubject->token[TOKEN_VALUE]; 30 | $p->error("Please remove literal boolean: `$bool` Try: `match { ... }`", $sMatchSubject->token); 31 | } 32 | $this->addKid($sMatchSubject); 33 | } 34 | else { 35 | // implicit true if no subject 36 | $sTrue = $p->makeSymbol(TokenType::WORD, 'true', SymbolType::BOOLEAN); 37 | $this->addKid($sTrue); 38 | } 39 | 40 | $p->now('{', 'match.open')->space(' {B'); 41 | 42 | $p->next(); 43 | 44 | // Collect pattern pairs. "pattern: value" 45 | $pos = 0; 46 | $hasDefaultCase = false; 47 | while (true) { 48 | 49 | $p->parseElementSeparator($pos, true, '}'); 50 | $pos += 1; 51 | 52 | if ($p->symbol->isValue("}")) { break; } 53 | 54 | // Pattern(s) 55 | $sMatchPatternList = $this->getMatchPatternList($p); 56 | 57 | // Colon ':' 58 | $p->now(':', 'match.colon')->space('x:S')->next(); 59 | 60 | // Match Value 61 | $sMatchValue = $p->parseExpression(0); 62 | 63 | $sMatchPair = $p->makeSymbol(SymbolType::MATCH_PAIR, '(pair)', SymbolType::MATCH_PAIR); 64 | 65 | $sMatchPair->addKid($sMatchPatternList); 66 | $sMatchPair->addKid($sMatchValue); 67 | 68 | $this->addKid($sMatchPair); 69 | } 70 | 71 | $p->now('}', 'match.close')->space(' } ')->next(); 72 | 73 | return $this; 74 | } 75 | 76 | function getMatchPatternList($p) { 77 | 78 | // Match Patterns (comma delimited) 79 | $matchPatterns = []; 80 | 81 | if ($p->symbol->isValue('default')) { 82 | $sMatchPattern = $p->makeSymbol(TokenType::WORD, 'default', SymbolType::CONSTANT); 83 | $matchPatterns []= $sMatchPattern; 84 | $p->next(); 85 | } 86 | else { 87 | while (true) { 88 | $sMatchPattern = $p->parseExpression(0); 89 | $matchPatterns []= $sMatchPattern; 90 | if (!$p->symbol->isValue(",")) { break; } 91 | $p->next(); 92 | } 93 | } 94 | 95 | $sMatchPatternList = $p->makeAstList(AstList::MATCH, $matchPatterns); 96 | 97 | return $sMatchPatternList; 98 | } 99 | } 100 | --------------------------------------------------------------------------------