From fbd4e92e9f95041322a91ffff4f8a48e128b1697 Mon Sep 17 00:00:00 2001 From: code Date: Tue, 14 Jul 2026 01:58:25 +0000 Subject: [PATCH] Replace v1 with v2 codebase Full rewrite: swap out the v1 framework (src/, controllers/, views/, twig/, sass/, skeleton/) for the working v2 codebase from phpproject (App/, novaconium/, public/). --- .gitignore | 3 + AGENTS.md | 277 +- App/config.php | 23 + App/lib/.gitkeep | 0 App/pages/about/index.twig | 38 + App/pages/blog/_layout/layout.twig | 15 + App/pages/blog/hello-world/index.twig | 26 + App/pages/blog/index.php | 30 + App/pages/blog/index.twig | 30 + App/pages/blog/second-post/index.twig | 26 + App/pages/blog/style-guide/index.twig | 128 + App/pages/blog/twig-syntax-guide/index.twig | 113 + App/pages/contact/index.php | 54 + App/pages/contact/index.twig | 63 + App/pages/index.twig | 77 + App/sass/_colors.sass | 25 + LICENSE | 9 - README.md | 99 +- _assets/novaconium-logo.png | Bin 50154 -> 0 bytes composer.json | 28 - config/routes.php | 53 - controllers/404.php | 4 - controllers/auth/login.php | 11 - controllers/auth/logout.php | 5 - controllers/authenticate.php | 70 - controllers/coming-soon.php | 9 - controllers/create_admin.php | 60 - controllers/dashboard.php | 14 - controllers/editpage.php | 85 - controllers/init.php | 202 -- controllers/message_delete.php | 15 - controllers/message_edit.php | 19 - controllers/message_save.php | 57 - controllers/messages.php | 22 - controllers/pages.php | 21 - controllers/samples.php | 34 - controllers/savepage.php | 116 - controllers/settings.php | 15 - controllers/sitemap.php | 42 - docs/404.md | 6 - docs/Composer.md | 18 - docs/ConfigurationFile.md | 52 - docs/Dev-Fake_autoload.md | 18 - docs/Logs.md | 19 - docs/Messages.md | 3 - docs/Post.md | 5 - docs/Redirect.md | 6 - docs/Sass.md | 33 - docs/Session.md | 5 - docs/StyleSheets-sass.md | 7 - docs/Twig-Views.md | 21 - docs/docker.md | 9 - novaconium/ISSUES.md | 442 ++++ novaconium/autoload.php | 96 + novaconium/bin/clear-cache.php | 16 + novaconium/bin/create-static-page.php | 98 + novaconium/bootstrap.php | 92 + novaconium/config.php | 41 + novaconium/lib/Csrf.php | 73 + novaconium/lib/FormValidator.php | 76 + novaconium/lib/Input.php | 87 + novaconium/lib/Mailer.php | 23 + novaconium/lib/SpamGuard.php | 51 + novaconium/lib/Validate.php | 116 + novaconium/pages/404/index.twig | 14 + novaconium/pages/_layout/icons.twig | 146 ++ novaconium/pages/_layout/layout.twig | 40 + novaconium/pages/_layout/matomo.twig | 17 + novaconium/pages/_layout/nav.twig | 25 + novaconium/pages/_layout/theme-init.twig | 12 + novaconium/pages/admin/clear-cache/index.php | 22 + novaconium/pages/admin/clear-cache/index.twig | 26 + .../pages/admin/docs/_layout/layout.twig | 32 + .../pages/admin/docs/admin-auth/index.twig | 50 + .../pages/admin/docs/caching/index.twig | 22 + novaconium/pages/admin/docs/config/index.twig | 71 + .../pages/admin/docs/design-notes/index.twig | 84 + novaconium/pages/admin/docs/forms/index.twig | 137 + .../admin/docs/getting-started/index.twig | 38 + novaconium/pages/admin/docs/index.twig | 32 + .../pages/admin/docs/layouts/index.twig | 28 + .../pages/admin/docs/libraries/index.twig | 30 + novaconium/pages/admin/docs/matomo/index.twig | 46 + .../admin/docs/project-layout/index.twig | 56 + .../pages/admin/docs/routing/index.twig | 73 + novaconium/pages/admin/docs/seo/index.twig | 94 + .../pages/admin/docs/sidecars/index.twig | 242 ++ .../pages/admin/docs/styling/index.twig | 60 + .../pages/admin/docs/third-party/index.twig | 13 + .../admin/docs/upgrading-twig/index.twig | 44 + novaconium/pages/admin/index.twig | 21 + .../pages/admin/password-hash/index.php | 38 + .../pages/admin/password-hash/index.twig | 42 + novaconium/sass/defaults/_colors.sass | 27 + novaconium/sass/main.sass | 392 +++ novaconium/src/AdminAuth.php | 63 + novaconium/src/Cache.php | 67 + novaconium/src/Overlay.php | 76 + novaconium/src/Renderer.php | 129 + novaconium/src/Response.php | 59 + novaconium/src/Route.php | 26 + novaconium/src/Router.php | 57 + novaconium/vendor/twig/LICENSE | 27 + .../vendor/twig/src/AbstractTwigCallable.php | 199 ++ .../twig/src/Attribute/AsTwigFilter.php | 60 + .../twig/src/Attribute/AsTwigFunction.php | 56 + .../vendor/twig/src/Attribute/AsTwigTest.php | 52 + .../Attribute/FirstClassTwigCallableReady.php | 20 + .../vendor/twig/src/Attribute/YieldReady.php | 20 + .../vendor/twig/src/Cache/CacheInterface.php | 46 + .../vendor/twig/src/Cache/ChainCache.php | 88 + .../vendor/twig/src/Cache/FilesystemCache.php | 95 + .../vendor/twig/src/Cache/NullCache.php | 42 + .../src/Cache/ReadOnlyFilesystemCache.php | 25 + .../src/Cache/RemovableCacheInterface.php | 20 + novaconium/vendor/twig/src/Compiler.php | 263 ++ .../twig/src/DeprecatedCallableInfo.php | 67 + novaconium/vendor/twig/src/Environment.php | 953 +++++++ novaconium/vendor/twig/src/Error/Error.php | 208 ++ .../vendor/twig/src/Error/LoaderError.php | 21 + .../vendor/twig/src/Error/RuntimeError.php | 22 + .../vendor/twig/src/Error/SyntaxError.php | 46 + .../vendor/twig/src/ExpressionParser.php | 369 +++ .../AbstractExpressionParser.php | 35 + .../ExpressionParserDescriptionInterface.php | 17 + .../ExpressionParserInterface.php | 36 + .../ExpressionParser/ExpressionParserType.php | 33 + .../ExpressionParser/ExpressionParsers.php | 155 ++ .../ExpressionParser/Infix/ArgumentsTrait.php | 83 + .../Infix/ArrowExpressionParser.php | 53 + .../Infix/AssignmentExpressionParser.php | 66 + .../Infix/BinaryOperatorExpressionParser.php | 80 + .../ConditionalTernaryExpressionParser.php | 62 + .../Infix/DotExpressionParser.php | 114 + .../Infix/FilterExpressionParser.php | 85 + .../Infix/FunctionExpressionParser.php | 97 + .../Infix/IsExpressionParser.php | 84 + .../Infix/IsNotExpressionParser.php | 33 + .../Infix/SquareBracketExpressionParser.php | 91 + .../ExpressionParser/InfixAssociativity.php | 18 + .../InfixExpressionParserInterface.php | 27 + .../src/ExpressionParser/PrecedenceChange.php | 42 + .../Prefix/GroupingExpressionParser.php | 88 + .../Prefix/LiteralExpressionParser.php | 233 ++ .../Prefix/UnaryOperatorExpressionParser.php | 72 + .../PrefixExpressionParserInterface.php | 25 + .../twig/src/Extension/AbstractExtension.php | 67 + .../twig/src/Extension/AttributeExtension.php | 179 ++ .../twig/src/Extension/CoreExtension.php | 2223 +++++++++++++++++ .../twig/src/Extension/DebugExtension.php | 62 + .../twig/src/Extension/EscaperExtension.php | 221 ++ .../twig/src/Extension/ExtensionInterface.php | 80 + .../twig/src/Extension/GlobalsInterface.php | 25 + .../LastModifiedExtensionInterface.php | 23 + .../twig/src/Extension/OptimizerExtension.php | 27 + .../twig/src/Extension/ProfilerExtension.php | 52 + .../Extension/RuntimeExtensionInterface.php | 19 + .../twig/src/Extension/SandboxExtension.php | 247 ++ .../twig/src/Extension/StagingExtension.php | 100 + .../src/Extension/StringLoaderExtension.php | 43 + .../src/Extension/YieldNotReadyExtension.php | 30 + novaconium/vendor/twig/src/ExtensionSet.php | 567 +++++ .../src/FileExtensionEscapingStrategy.php | 61 + novaconium/vendor/twig/src/Lexer.php | 621 +++++ .../vendor/twig/src/Loader/ArrayLoader.php | 75 + .../vendor/twig/src/Loader/ChainLoader.php | 132 + .../twig/src/Loader/FilesystemLoader.php | 290 +++ .../twig/src/Loader/LoaderInterface.php | 49 + novaconium/vendor/twig/src/Markup.php | 67 + .../vendor/twig/src/Node/AutoEscapeNode.php | 40 + novaconium/vendor/twig/src/Node/BlockNode.php | 50 + .../twig/src/Node/BlockReferenceNode.php | 38 + novaconium/vendor/twig/src/Node/BodyNode.php | 24 + .../vendor/twig/src/Node/CaptureNode.php | 57 + .../twig/src/Node/CheckSecurityCallNode.php | 32 + .../twig/src/Node/CheckSecurityNode.php | 109 + .../twig/src/Node/CheckToStringNode.php | 48 + .../Node/CoercesChildrenToStringInterface.php | 40 + .../vendor/twig/src/Node/ConfigNode.php | 30 + .../vendor/twig/src/Node/DeprecatedNode.php | 87 + novaconium/vendor/twig/src/Node/DoNode.php | 40 + novaconium/vendor/twig/src/Node/EmbedNode.php | 54 + novaconium/vendor/twig/src/Node/EmptyNode.php | 33 + .../Node/Expression/AbstractExpression.php | 43 + .../src/Node/Expression/ArrayExpression.php | 165 ++ .../Expression/ArrowFunctionExpression.php | 63 + .../Node/Expression/AssignNameExpression.php | 44 + .../Node/Expression/Binary/AbstractBinary.php | 53 + .../src/Node/Expression/Binary/AddBinary.php | 24 + .../src/Node/Expression/Binary/AndBinary.php | 31 + .../Expression/Binary/BinaryInterface.php | 22 + .../Expression/Binary/BitwiseAndBinary.php | 24 + .../Expression/Binary/BitwiseOrBinary.php | 24 + .../Expression/Binary/BitwiseXorBinary.php | 24 + .../Node/Expression/Binary/ConcatBinary.php | 30 + .../src/Node/Expression/Binary/DivBinary.php | 24 + .../Node/Expression/Binary/ElvisBinary.php | 56 + .../Node/Expression/Binary/EndsWithBinary.php | 36 + .../Node/Expression/Binary/EqualBinary.php | 46 + .../Node/Expression/Binary/FloorDivBinary.php | 30 + .../Node/Expression/Binary/GreaterBinary.php | 46 + .../Expression/Binary/GreaterEqualBinary.php | 46 + .../Node/Expression/Binary/HasEveryBinary.php | 34 + .../Node/Expression/Binary/HasSomeBinary.php | 34 + .../src/Node/Expression/Binary/InBinary.php | 40 + .../src/Node/Expression/Binary/LessBinary.php | 46 + .../Expression/Binary/LessEqualBinary.php | 46 + .../Node/Expression/Binary/MatchesBinary.php | 66 + .../src/Node/Expression/Binary/ModBinary.php | 24 + .../src/Node/Expression/Binary/MulBinary.php | 24 + .../Node/Expression/Binary/NotEqualBinary.php | 46 + .../Node/Expression/Binary/NotInBinary.php | 40 + .../Expression/Binary/NotSameAsBinary.php | 23 + .../Expression/Binary/NullCoalesceBinary.php | 71 + .../Binary/ObjectDestructuringSetBinary.php | 77 + .../src/Node/Expression/Binary/OrBinary.php | 31 + .../Node/Expression/Binary/PowerBinary.php | 23 + .../Node/Expression/Binary/RangeBinary.php | 40 + .../Node/Expression/Binary/SameAsBinary.php | 23 + .../Binary/SequenceDestructuringSetBinary.php | 67 + .../src/Node/Expression/Binary/SetBinary.php | 44 + .../Expression/Binary/SpaceshipBinary.php | 29 + .../Expression/Binary/StartsWithBinary.php | 36 + .../src/Node/Expression/Binary/SubBinary.php | 24 + .../src/Node/Expression/Binary/XorBinary.php | 31 + .../Expression/BlockReferenceExpression.php | 102 + .../src/Node/Expression/CallExpression.php | 375 +++ .../Node/Expression/ConditionalExpression.php | 53 + .../Node/Expression/ConstantExpression.php | 33 + .../src/Node/Expression/EmptyExpression.php | 33 + .../Node/Expression/Filter/DefaultFilter.php | 72 + .../src/Node/Expression/Filter/RawFilter.php | 45 + .../src/Node/Expression/FilterExpression.php | 87 + .../Node/Expression/FunctionExpression.php | 88 + .../FunctionNode/EnumCasesFunction.php | 50 + .../Expression/FunctionNode/EnumFunction.php | 54 + .../src/Node/Expression/GetAttrExpression.php | 228 ++ .../twig/src/Node/Expression/InlinePrint.php | 39 + .../src/Node/Expression/ListExpression.php | 41 + .../Expression/MacroReferenceExpression.php | 136 + .../Node/Expression/MethodCallExpression.php | 58 + .../src/Node/Expression/NameExpression.php | 115 + .../Expression/NullCoalesceExpression.php | 77 + .../Expression/OperatorEscapeInterface.php | 25 + .../src/Node/Expression/ParentExpression.php | 46 + .../Node/Expression/ReturnArrayInterface.php | 16 + .../Node/Expression/ReturnBoolInterface.php | 16 + .../Node/Expression/ReturnNumberInterface.php | 16 + .../ReturnPrimitiveTypeInterface.php | 16 + .../Node/Expression/ReturnStringInterface.php | 16 + .../SupportDefinedTestDeprecationTrait.php | 44 + .../SupportDefinedTestInterface.php | 24 + .../Expression/SupportDefinedTestTrait.php | 27 + .../Node/Expression/TempNameExpression.php | 49 + .../Expression/Ternary/ConditionalTernary.php | 43 + .../src/Node/Expression/Test/ConstantTest.php | 49 + .../src/Node/Expression/Test/DefinedTest.php | 68 + .../Node/Expression/Test/DivisiblebyTest.php | 42 + .../src/Node/Expression/Test/EvenTest.php | 41 + .../src/Node/Expression/Test/NullTest.php | 40 + .../twig/src/Node/Expression/Test/OddTest.php | 41 + .../src/Node/Expression/Test/SameasTest.php | 40 + .../src/Node/Expression/Test/TrueTest.php | 52 + .../src/Node/Expression/TestExpression.php | 91 + .../Node/Expression/Unary/AbstractUnary.php | 48 + .../src/Node/Expression/Unary/NegUnary.php | 23 + .../src/Node/Expression/Unary/NotUnary.php | 31 + .../src/Node/Expression/Unary/PosUnary.php | 23 + .../src/Node/Expression/Unary/SpreadUnary.php | 22 + .../Node/Expression/Unary/StringCastUnary.php | 22 + .../Node/Expression/Unary/UnaryInterface.php | 22 + .../Variable/AssignContextVariable.php | 18 + .../Variable/AssignTemplateVariable.php | 44 + .../Expression/Variable/ContextVariable.php | 18 + .../Expression/Variable/LocalVariable.php | 18 + .../Expression/Variable/TemplateVariable.php | 42 + .../Node/Expression/VariadicExpression.php | 24 + novaconium/vendor/twig/src/Node/FlushNode.php | 40 + .../vendor/twig/src/Node/ForElseNode.php | 41 + .../vendor/twig/src/Node/ForLoopNode.php | 51 + novaconium/vendor/twig/src/Node/ForNode.php | 123 + novaconium/vendor/twig/src/Node/IfNode.php | 79 + .../vendor/twig/src/Node/ImportNode.php | 67 + .../vendor/twig/src/Node/IncludeNode.php | 121 + novaconium/vendor/twig/src/Node/MacroNode.php | 121 + .../vendor/twig/src/Node/ModuleNode.php | 522 ++++ .../vendor/twig/src/Node/NameDeprecation.php | 46 + novaconium/vendor/twig/src/Node/Node.php | 294 +++ .../twig/src/Node/NodeCaptureInterface.php | 21 + .../twig/src/Node/NodeOutputInterface.php | 21 + novaconium/vendor/twig/src/Node/Nodes.php | 28 + novaconium/vendor/twig/src/Node/PrintNode.php | 69 + .../vendor/twig/src/Node/SandboxNode.php | 54 + novaconium/vendor/twig/src/Node/SetNode.php | 115 + novaconium/vendor/twig/src/Node/TextNode.php | 57 + novaconium/vendor/twig/src/Node/TypesNode.php | 40 + novaconium/vendor/twig/src/Node/WithNode.php | 72 + novaconium/vendor/twig/src/NodeTraverser.php | 76 + .../src/NodeVisitor/AbstractNodeVisitor.php | 49 + .../NodeVisitor/CorrectnessNodeVisitor.php | 223 ++ .../src/NodeVisitor/EscaperNodeVisitor.php | 185 ++ .../src/NodeVisitor/NodeVisitorInterface.php | 46 + .../src/NodeVisitor/OptimizerNodeVisitor.php | 213 ++ .../NodeVisitor/SafeAnalysisNodeVisitor.php | 185 ++ .../src/NodeVisitor/SandboxNodeVisitor.php | 295 +++ .../NodeVisitor/YieldNotReadyNodeVisitor.php | 59 + .../twig/src/OperatorPrecedenceChange.php | 34 + novaconium/vendor/twig/src/Parser.php | 612 +++++ .../twig/src/Profiler/Dumper/BaseDumper.php | 68 + .../src/Profiler/Dumper/BlackfireDumper.php | 72 + .../twig/src/Profiler/Dumper/HtmlDumper.php | 57 + .../twig/src/Profiler/Dumper/TextDumper.php | 35 + .../src/Profiler/Node/EnterProfileNode.php | 44 + .../src/Profiler/Node/LeaveProfileNode.php | 38 + .../NodeVisitor/ProfilerNodeVisitor.php | 70 + .../vendor/twig/src/Profiler/Profile.php | 194 ++ novaconium/vendor/twig/src/Resources/core.php | 572 +++++ .../vendor/twig/src/Resources/debug.php | 25 + .../vendor/twig/src/Resources/escaper.php | 51 + .../twig/src/Resources/string_loader.php | 26 + .../twig/src/Runtime/EscaperRuntime.php | 346 +++ .../RuntimeLoader/ContainerRuntimeLoader.php | 35 + .../RuntimeLoader/FactoryRuntimeLoader.php | 39 + .../RuntimeLoader/RuntimeLoaderInterface.php | 27 + .../vendor/twig/src/Sandbox/SecurityError.php | 23 + .../Sandbox/SecurityNotAllowedFilterError.php | 33 + .../SecurityNotAllowedFunctionError.php | 33 + .../Sandbox/SecurityNotAllowedMethodError.php | 40 + .../SecurityNotAllowedPropertyError.php | 40 + .../Sandbox/SecurityNotAllowedTagError.php | 33 + .../Sandbox/SecurityNotAllowedTestError.php | 33 + .../twig/src/Sandbox/SecurityPolicy.php | 175 ++ .../src/Sandbox/SecurityPolicyInterface.php | 46 + .../src/Sandbox/SourcePolicyInterface.php | 26 + novaconium/vendor/twig/src/Source.php | 66 + novaconium/vendor/twig/src/Template.php | 560 +++++ .../vendor/twig/src/TemplateWrapper.php | 112 + .../twig/src/Test/IntegrationTestCase.php | 380 +++ .../vendor/twig/src/Test/NodeTestCase.php | 144 ++ novaconium/vendor/twig/src/Token.php | 256 ++ .../src/TokenParser/AbstractTokenParser.php | 66 + .../twig/src/TokenParser/ApplyTokenParser.php | 66 + .../src/TokenParser/AutoEscapeTokenParser.php | 58 + .../twig/src/TokenParser/BlockTokenParser.php | 77 + .../src/TokenParser/DeprecatedTokenParser.php | 65 + .../twig/src/TokenParser/DoTokenParser.php | 38 + .../twig/src/TokenParser/EmbedTokenParser.php | 73 + .../src/TokenParser/ExtendsTokenParser.php | 41 + .../twig/src/TokenParser/FlushTokenParser.php | 38 + .../twig/src/TokenParser/ForTokenParser.php | 80 + .../twig/src/TokenParser/FromTokenParser.php | 69 + .../twig/src/TokenParser/GuardTokenParser.php | 79 + .../twig/src/TokenParser/IfTokenParser.php | 90 + .../src/TokenParser/ImportTokenParser.php | 45 + .../src/TokenParser/IncludeTokenParser.php | 73 + .../twig/src/TokenParser/MacroTokenParser.php | 123 + .../src/TokenParser/SandboxTokenParser.php | 72 + .../twig/src/TokenParser/SetTokenParser.php | 87 + .../src/TokenParser/TokenParserInterface.php | 48 + .../twig/src/TokenParser/TypesTokenParser.php | 89 + .../twig/src/TokenParser/UseTokenParser.php | 75 + .../twig/src/TokenParser/WithTokenParser.php | 56 + novaconium/vendor/twig/src/TokenStream.php | 132 + .../vendor/twig/src/TwigCallableInterface.php | 56 + novaconium/vendor/twig/src/TwigFilter.php | 74 + novaconium/vendor/twig/src/TwigFunction.php | 63 + novaconium/vendor/twig/src/TwigTest.php | 67 + .../src/Util/CallableArgumentsExtractor.php | 204 ++ .../twig/src/Util/CallableParameters.php | 161 ++ .../twig/src/Util/DeprecationCollector.php | 77 + .../twig/src/Util/ReflectionCallable.php | 131 + .../twig/src/Util/TemplateDirIterator.php | 36 + public/.htaccess | 17 + public/css/main.css | 433 ++++ public/index.php | 3 + public/router.php | 23 + sass/Dockerfile | 12 - sass/abstracts/_variables.sass | 80 - sass/abstracts/index.sass | 1 - sass/base/_background.sass | 6 - sass/base/_reset.sass | 178 -- sass/base/index.sass | 2 - sass/coming-soon/index.sass | 94 - sass/controlPanel/_footer.sass | 4 - sass/controlPanel/_header.sass | 8 - sass/controlPanel/_main.sass | 11 - sass/controlPanel/_menu.sass | 22 - sass/controlPanel/_panel.sass | 5 - sass/controlPanel/index.sass | 10 - sass/framework/_ace.sass | 29 - sass/framework/_edit_page.sass | 15 - sass/framework/_forms.sass | 62 - sass/framework/_login_form.sass | 53 - sass/framework/_logo.sass | 79 - sass/framework/_main.sass | 62 - sass/framework/_tabs.sass | 49 - sass/framework/_tags.sass | 109 - sass/framework/_tooltip.sass | 48 - sass/framework/_ui.sass | 59 - sass/framework/index.sass | 10 - sass/novaconium.sass | 6 - skeleton/.env | 2 - skeleton/.gitignore | 3 - skeleton/docker-compose.yml | 53 - skeleton/novaconium/App/config.php | 17 - .../novaconium/App/controllers/humans.php | 28 - skeleton/novaconium/App/controllers/index.php | 7 - skeleton/novaconium/App/controllers/page.php | 33 - .../novaconium/App/controllers/robots.php | 18 - skeleton/novaconium/App/routes.php | 15 - .../App/templates/override.html.twig | 1 - skeleton/novaconium/App/views/index.html.twig | 21 - skeleton/novaconium/App/views/page.html.twig | 11 - skeleton/novaconium/public/.htaccess | 4 - skeleton/novaconium/public/css/novaconium.css | 1 - skeleton/novaconium/public/index.php | 19 - skeleton/novaconium/public/js/tabs.js | 185 -- skeleton/novaconium/sass/project.sass | 1 - src/Database.php | 127 - src/Logger.php | 54 - src/MessageHandler.php | 87 - src/Post.php | 25 - src/Redirect.php | 40 - src/Router.php | 142 -- src/Services/Auth.php | 10 - src/Services/TagManager.php | 53 - src/Session.php | 63 - src/functions.php | 60 - src/novaconium.php | 59 - src/twig.php | 69 - twig/coming-soon/index.html.twig | 71 - twig/cp/control-panel.html.twig | 44 - twig/cp/head.html.twig | 60 - twig/cp/menu.html.twig | 9 - twig/foot.html.twig | 5 - twig/footer.html.twig | 1 - twig/head.html.twig | 48 - twig/javascript/ace.html.twig | 40 - twig/javascript/page-edit.html.twig | 192 -- twig/left.html.twig | 16 - twig/master.html.twig | 91 - twig/nav.html.twig | 5 - twig/right.html.twig | 1 - views/404.html.twig | 8 - views/auth/login.html.twig | 23 - views/coming-soon.html.twig | 32 - views/dashboard.html.twig | 8 - views/editmessage.html.twig | 30 - views/editpage/index.html.twig | 62 - views/editpage/tab-main.html.twig | 29 - views/editpage/tab-metadata.html.twig | 17 - views/editpage/tab-notes.html.twig | 4 - views/editpage/tab-other.html.twig | 25 - views/editpage/tab-tags.html.twig | 12 - views/editpage/tab-tweaks.html.twig | 10 - views/init.html.twig | 67 - views/messages.html.twig | 43 - views/pages.html.twig | 30 - views/samples/basic.html.twig | 269 -- views/settings.html.twig | 6 - 460 files changed, 30107 insertions(+), 4661 deletions(-) create mode 100644 App/config.php create mode 100644 App/lib/.gitkeep create mode 100644 App/pages/about/index.twig create mode 100644 App/pages/blog/_layout/layout.twig create mode 100644 App/pages/blog/hello-world/index.twig create mode 100644 App/pages/blog/index.php create mode 100644 App/pages/blog/index.twig create mode 100644 App/pages/blog/second-post/index.twig create mode 100644 App/pages/blog/style-guide/index.twig create mode 100644 App/pages/blog/twig-syntax-guide/index.twig create mode 100644 App/pages/contact/index.php create mode 100644 App/pages/contact/index.twig create mode 100644 App/pages/index.twig create mode 100644 App/sass/_colors.sass delete mode 100644 LICENSE delete mode 100644 _assets/novaconium-logo.png delete mode 100644 composer.json delete mode 100644 config/routes.php delete mode 100644 controllers/404.php delete mode 100644 controllers/auth/login.php delete mode 100644 controllers/auth/logout.php delete mode 100644 controllers/authenticate.php delete mode 100644 controllers/coming-soon.php delete mode 100644 controllers/create_admin.php delete mode 100644 controllers/dashboard.php delete mode 100644 controllers/editpage.php delete mode 100644 controllers/init.php delete mode 100644 controllers/message_delete.php delete mode 100644 controllers/message_edit.php delete mode 100644 controllers/message_save.php delete mode 100644 controllers/messages.php delete mode 100644 controllers/pages.php delete mode 100644 controllers/samples.php delete mode 100644 controllers/savepage.php delete mode 100644 controllers/settings.php delete mode 100644 controllers/sitemap.php delete mode 100644 docs/404.md delete mode 100644 docs/Composer.md delete mode 100644 docs/ConfigurationFile.md delete mode 100644 docs/Dev-Fake_autoload.md delete mode 100644 docs/Logs.md delete mode 100644 docs/Messages.md delete mode 100644 docs/Post.md delete mode 100644 docs/Redirect.md delete mode 100644 docs/Sass.md delete mode 100644 docs/Session.md delete mode 100644 docs/StyleSheets-sass.md delete mode 100644 docs/Twig-Views.md delete mode 100644 docs/docker.md create mode 100644 novaconium/ISSUES.md create mode 100644 novaconium/autoload.php create mode 100644 novaconium/bin/clear-cache.php create mode 100644 novaconium/bin/create-static-page.php create mode 100644 novaconium/bootstrap.php create mode 100644 novaconium/config.php create mode 100644 novaconium/lib/Csrf.php create mode 100644 novaconium/lib/FormValidator.php create mode 100644 novaconium/lib/Input.php create mode 100644 novaconium/lib/Mailer.php create mode 100644 novaconium/lib/SpamGuard.php create mode 100644 novaconium/lib/Validate.php create mode 100644 novaconium/pages/404/index.twig create mode 100644 novaconium/pages/_layout/icons.twig create mode 100644 novaconium/pages/_layout/layout.twig create mode 100644 novaconium/pages/_layout/matomo.twig create mode 100644 novaconium/pages/_layout/nav.twig create mode 100644 novaconium/pages/_layout/theme-init.twig create mode 100644 novaconium/pages/admin/clear-cache/index.php create mode 100644 novaconium/pages/admin/clear-cache/index.twig create mode 100644 novaconium/pages/admin/docs/_layout/layout.twig create mode 100644 novaconium/pages/admin/docs/admin-auth/index.twig create mode 100644 novaconium/pages/admin/docs/caching/index.twig create mode 100644 novaconium/pages/admin/docs/config/index.twig create mode 100644 novaconium/pages/admin/docs/design-notes/index.twig create mode 100644 novaconium/pages/admin/docs/forms/index.twig create mode 100644 novaconium/pages/admin/docs/getting-started/index.twig create mode 100644 novaconium/pages/admin/docs/index.twig create mode 100644 novaconium/pages/admin/docs/layouts/index.twig create mode 100644 novaconium/pages/admin/docs/libraries/index.twig create mode 100644 novaconium/pages/admin/docs/matomo/index.twig create mode 100644 novaconium/pages/admin/docs/project-layout/index.twig create mode 100644 novaconium/pages/admin/docs/routing/index.twig create mode 100644 novaconium/pages/admin/docs/seo/index.twig create mode 100644 novaconium/pages/admin/docs/sidecars/index.twig create mode 100644 novaconium/pages/admin/docs/styling/index.twig create mode 100644 novaconium/pages/admin/docs/third-party/index.twig create mode 100644 novaconium/pages/admin/docs/upgrading-twig/index.twig create mode 100644 novaconium/pages/admin/index.twig create mode 100644 novaconium/pages/admin/password-hash/index.php create mode 100644 novaconium/pages/admin/password-hash/index.twig create mode 100644 novaconium/sass/defaults/_colors.sass create mode 100644 novaconium/sass/main.sass create mode 100644 novaconium/src/AdminAuth.php create mode 100644 novaconium/src/Cache.php create mode 100644 novaconium/src/Overlay.php create mode 100644 novaconium/src/Renderer.php create mode 100644 novaconium/src/Response.php create mode 100644 novaconium/src/Route.php create mode 100644 novaconium/src/Router.php create mode 100644 novaconium/vendor/twig/LICENSE create mode 100644 novaconium/vendor/twig/src/AbstractTwigCallable.php create mode 100644 novaconium/vendor/twig/src/Attribute/AsTwigFilter.php create mode 100644 novaconium/vendor/twig/src/Attribute/AsTwigFunction.php create mode 100644 novaconium/vendor/twig/src/Attribute/AsTwigTest.php create mode 100644 novaconium/vendor/twig/src/Attribute/FirstClassTwigCallableReady.php create mode 100644 novaconium/vendor/twig/src/Attribute/YieldReady.php create mode 100644 novaconium/vendor/twig/src/Cache/CacheInterface.php create mode 100644 novaconium/vendor/twig/src/Cache/ChainCache.php create mode 100644 novaconium/vendor/twig/src/Cache/FilesystemCache.php create mode 100644 novaconium/vendor/twig/src/Cache/NullCache.php create mode 100644 novaconium/vendor/twig/src/Cache/ReadOnlyFilesystemCache.php create mode 100644 novaconium/vendor/twig/src/Cache/RemovableCacheInterface.php create mode 100644 novaconium/vendor/twig/src/Compiler.php create mode 100644 novaconium/vendor/twig/src/DeprecatedCallableInfo.php create mode 100644 novaconium/vendor/twig/src/Environment.php create mode 100644 novaconium/vendor/twig/src/Error/Error.php create mode 100644 novaconium/vendor/twig/src/Error/LoaderError.php create mode 100644 novaconium/vendor/twig/src/Error/RuntimeError.php create mode 100644 novaconium/vendor/twig/src/Error/SyntaxError.php create mode 100644 novaconium/vendor/twig/src/ExpressionParser.php create mode 100644 novaconium/vendor/twig/src/ExpressionParser/AbstractExpressionParser.php create mode 100644 novaconium/vendor/twig/src/ExpressionParser/ExpressionParserDescriptionInterface.php create mode 100644 novaconium/vendor/twig/src/ExpressionParser/ExpressionParserInterface.php create mode 100644 novaconium/vendor/twig/src/ExpressionParser/ExpressionParserType.php create mode 100644 novaconium/vendor/twig/src/ExpressionParser/ExpressionParsers.php create mode 100644 novaconium/vendor/twig/src/ExpressionParser/Infix/ArgumentsTrait.php create mode 100644 novaconium/vendor/twig/src/ExpressionParser/Infix/ArrowExpressionParser.php create mode 100644 novaconium/vendor/twig/src/ExpressionParser/Infix/AssignmentExpressionParser.php create mode 100644 novaconium/vendor/twig/src/ExpressionParser/Infix/BinaryOperatorExpressionParser.php create mode 100644 novaconium/vendor/twig/src/ExpressionParser/Infix/ConditionalTernaryExpressionParser.php create mode 100644 novaconium/vendor/twig/src/ExpressionParser/Infix/DotExpressionParser.php create mode 100644 novaconium/vendor/twig/src/ExpressionParser/Infix/FilterExpressionParser.php create mode 100644 novaconium/vendor/twig/src/ExpressionParser/Infix/FunctionExpressionParser.php create mode 100644 novaconium/vendor/twig/src/ExpressionParser/Infix/IsExpressionParser.php create mode 100644 novaconium/vendor/twig/src/ExpressionParser/Infix/IsNotExpressionParser.php create mode 100644 novaconium/vendor/twig/src/ExpressionParser/Infix/SquareBracketExpressionParser.php create mode 100644 novaconium/vendor/twig/src/ExpressionParser/InfixAssociativity.php create mode 100644 novaconium/vendor/twig/src/ExpressionParser/InfixExpressionParserInterface.php create mode 100644 novaconium/vendor/twig/src/ExpressionParser/PrecedenceChange.php create mode 100644 novaconium/vendor/twig/src/ExpressionParser/Prefix/GroupingExpressionParser.php create mode 100644 novaconium/vendor/twig/src/ExpressionParser/Prefix/LiteralExpressionParser.php create mode 100644 novaconium/vendor/twig/src/ExpressionParser/Prefix/UnaryOperatorExpressionParser.php create mode 100644 novaconium/vendor/twig/src/ExpressionParser/PrefixExpressionParserInterface.php create mode 100644 novaconium/vendor/twig/src/Extension/AbstractExtension.php create mode 100644 novaconium/vendor/twig/src/Extension/AttributeExtension.php create mode 100644 novaconium/vendor/twig/src/Extension/CoreExtension.php create mode 100644 novaconium/vendor/twig/src/Extension/DebugExtension.php create mode 100644 novaconium/vendor/twig/src/Extension/EscaperExtension.php create mode 100644 novaconium/vendor/twig/src/Extension/ExtensionInterface.php create mode 100644 novaconium/vendor/twig/src/Extension/GlobalsInterface.php create mode 100644 novaconium/vendor/twig/src/Extension/LastModifiedExtensionInterface.php create mode 100644 novaconium/vendor/twig/src/Extension/OptimizerExtension.php create mode 100644 novaconium/vendor/twig/src/Extension/ProfilerExtension.php create mode 100644 novaconium/vendor/twig/src/Extension/RuntimeExtensionInterface.php create mode 100644 novaconium/vendor/twig/src/Extension/SandboxExtension.php create mode 100644 novaconium/vendor/twig/src/Extension/StagingExtension.php create mode 100644 novaconium/vendor/twig/src/Extension/StringLoaderExtension.php create mode 100644 novaconium/vendor/twig/src/Extension/YieldNotReadyExtension.php create mode 100644 novaconium/vendor/twig/src/ExtensionSet.php create mode 100644 novaconium/vendor/twig/src/FileExtensionEscapingStrategy.php create mode 100644 novaconium/vendor/twig/src/Lexer.php create mode 100644 novaconium/vendor/twig/src/Loader/ArrayLoader.php create mode 100644 novaconium/vendor/twig/src/Loader/ChainLoader.php create mode 100644 novaconium/vendor/twig/src/Loader/FilesystemLoader.php create mode 100644 novaconium/vendor/twig/src/Loader/LoaderInterface.php create mode 100644 novaconium/vendor/twig/src/Markup.php create mode 100644 novaconium/vendor/twig/src/Node/AutoEscapeNode.php create mode 100644 novaconium/vendor/twig/src/Node/BlockNode.php create mode 100644 novaconium/vendor/twig/src/Node/BlockReferenceNode.php create mode 100644 novaconium/vendor/twig/src/Node/BodyNode.php create mode 100644 novaconium/vendor/twig/src/Node/CaptureNode.php create mode 100644 novaconium/vendor/twig/src/Node/CheckSecurityCallNode.php create mode 100644 novaconium/vendor/twig/src/Node/CheckSecurityNode.php create mode 100644 novaconium/vendor/twig/src/Node/CheckToStringNode.php create mode 100644 novaconium/vendor/twig/src/Node/CoercesChildrenToStringInterface.php create mode 100644 novaconium/vendor/twig/src/Node/ConfigNode.php create mode 100644 novaconium/vendor/twig/src/Node/DeprecatedNode.php create mode 100644 novaconium/vendor/twig/src/Node/DoNode.php create mode 100644 novaconium/vendor/twig/src/Node/EmbedNode.php create mode 100644 novaconium/vendor/twig/src/Node/EmptyNode.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/AbstractExpression.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/ArrayExpression.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/ArrowFunctionExpression.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/AssignNameExpression.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Binary/AbstractBinary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Binary/AddBinary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Binary/AndBinary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Binary/BinaryInterface.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Binary/BitwiseAndBinary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Binary/BitwiseOrBinary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Binary/BitwiseXorBinary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Binary/ConcatBinary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Binary/DivBinary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Binary/ElvisBinary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Binary/EndsWithBinary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Binary/EqualBinary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Binary/FloorDivBinary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Binary/GreaterBinary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Binary/GreaterEqualBinary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Binary/HasEveryBinary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Binary/HasSomeBinary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Binary/InBinary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Binary/LessBinary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Binary/LessEqualBinary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Binary/MatchesBinary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Binary/ModBinary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Binary/MulBinary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Binary/NotEqualBinary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Binary/NotInBinary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Binary/NotSameAsBinary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Binary/NullCoalesceBinary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Binary/ObjectDestructuringSetBinary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Binary/OrBinary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Binary/PowerBinary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Binary/RangeBinary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Binary/SameAsBinary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Binary/SequenceDestructuringSetBinary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Binary/SetBinary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Binary/SpaceshipBinary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Binary/StartsWithBinary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Binary/SubBinary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Binary/XorBinary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/BlockReferenceExpression.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/CallExpression.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/ConditionalExpression.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/ConstantExpression.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/EmptyExpression.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Filter/DefaultFilter.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Filter/RawFilter.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/FilterExpression.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/FunctionExpression.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/FunctionNode/EnumCasesFunction.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/FunctionNode/EnumFunction.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/GetAttrExpression.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/InlinePrint.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/ListExpression.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/MacroReferenceExpression.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/MethodCallExpression.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/NameExpression.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/NullCoalesceExpression.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/OperatorEscapeInterface.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/ParentExpression.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/ReturnArrayInterface.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/ReturnBoolInterface.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/ReturnNumberInterface.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/ReturnPrimitiveTypeInterface.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/ReturnStringInterface.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/SupportDefinedTestDeprecationTrait.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/SupportDefinedTestInterface.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/SupportDefinedTestTrait.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/TempNameExpression.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Ternary/ConditionalTernary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Test/ConstantTest.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Test/DefinedTest.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Test/DivisiblebyTest.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Test/EvenTest.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Test/NullTest.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Test/OddTest.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Test/SameasTest.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Test/TrueTest.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/TestExpression.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Unary/AbstractUnary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Unary/NegUnary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Unary/NotUnary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Unary/PosUnary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Unary/SpreadUnary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Unary/StringCastUnary.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Unary/UnaryInterface.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Variable/AssignContextVariable.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Variable/AssignTemplateVariable.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Variable/ContextVariable.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Variable/LocalVariable.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/Variable/TemplateVariable.php create mode 100644 novaconium/vendor/twig/src/Node/Expression/VariadicExpression.php create mode 100644 novaconium/vendor/twig/src/Node/FlushNode.php create mode 100644 novaconium/vendor/twig/src/Node/ForElseNode.php create mode 100644 novaconium/vendor/twig/src/Node/ForLoopNode.php create mode 100644 novaconium/vendor/twig/src/Node/ForNode.php create mode 100644 novaconium/vendor/twig/src/Node/IfNode.php create mode 100644 novaconium/vendor/twig/src/Node/ImportNode.php create mode 100644 novaconium/vendor/twig/src/Node/IncludeNode.php create mode 100644 novaconium/vendor/twig/src/Node/MacroNode.php create mode 100644 novaconium/vendor/twig/src/Node/ModuleNode.php create mode 100644 novaconium/vendor/twig/src/Node/NameDeprecation.php create mode 100644 novaconium/vendor/twig/src/Node/Node.php create mode 100644 novaconium/vendor/twig/src/Node/NodeCaptureInterface.php create mode 100644 novaconium/vendor/twig/src/Node/NodeOutputInterface.php create mode 100644 novaconium/vendor/twig/src/Node/Nodes.php create mode 100644 novaconium/vendor/twig/src/Node/PrintNode.php create mode 100644 novaconium/vendor/twig/src/Node/SandboxNode.php create mode 100644 novaconium/vendor/twig/src/Node/SetNode.php create mode 100644 novaconium/vendor/twig/src/Node/TextNode.php create mode 100644 novaconium/vendor/twig/src/Node/TypesNode.php create mode 100644 novaconium/vendor/twig/src/Node/WithNode.php create mode 100644 novaconium/vendor/twig/src/NodeTraverser.php create mode 100644 novaconium/vendor/twig/src/NodeVisitor/AbstractNodeVisitor.php create mode 100644 novaconium/vendor/twig/src/NodeVisitor/CorrectnessNodeVisitor.php create mode 100644 novaconium/vendor/twig/src/NodeVisitor/EscaperNodeVisitor.php create mode 100644 novaconium/vendor/twig/src/NodeVisitor/NodeVisitorInterface.php create mode 100644 novaconium/vendor/twig/src/NodeVisitor/OptimizerNodeVisitor.php create mode 100644 novaconium/vendor/twig/src/NodeVisitor/SafeAnalysisNodeVisitor.php create mode 100644 novaconium/vendor/twig/src/NodeVisitor/SandboxNodeVisitor.php create mode 100644 novaconium/vendor/twig/src/NodeVisitor/YieldNotReadyNodeVisitor.php create mode 100644 novaconium/vendor/twig/src/OperatorPrecedenceChange.php create mode 100644 novaconium/vendor/twig/src/Parser.php create mode 100644 novaconium/vendor/twig/src/Profiler/Dumper/BaseDumper.php create mode 100644 novaconium/vendor/twig/src/Profiler/Dumper/BlackfireDumper.php create mode 100644 novaconium/vendor/twig/src/Profiler/Dumper/HtmlDumper.php create mode 100644 novaconium/vendor/twig/src/Profiler/Dumper/TextDumper.php create mode 100644 novaconium/vendor/twig/src/Profiler/Node/EnterProfileNode.php create mode 100644 novaconium/vendor/twig/src/Profiler/Node/LeaveProfileNode.php create mode 100644 novaconium/vendor/twig/src/Profiler/NodeVisitor/ProfilerNodeVisitor.php create mode 100644 novaconium/vendor/twig/src/Profiler/Profile.php create mode 100644 novaconium/vendor/twig/src/Resources/core.php create mode 100644 novaconium/vendor/twig/src/Resources/debug.php create mode 100644 novaconium/vendor/twig/src/Resources/escaper.php create mode 100644 novaconium/vendor/twig/src/Resources/string_loader.php create mode 100644 novaconium/vendor/twig/src/Runtime/EscaperRuntime.php create mode 100644 novaconium/vendor/twig/src/RuntimeLoader/ContainerRuntimeLoader.php create mode 100644 novaconium/vendor/twig/src/RuntimeLoader/FactoryRuntimeLoader.php create mode 100644 novaconium/vendor/twig/src/RuntimeLoader/RuntimeLoaderInterface.php create mode 100644 novaconium/vendor/twig/src/Sandbox/SecurityError.php create mode 100644 novaconium/vendor/twig/src/Sandbox/SecurityNotAllowedFilterError.php create mode 100644 novaconium/vendor/twig/src/Sandbox/SecurityNotAllowedFunctionError.php create mode 100644 novaconium/vendor/twig/src/Sandbox/SecurityNotAllowedMethodError.php create mode 100644 novaconium/vendor/twig/src/Sandbox/SecurityNotAllowedPropertyError.php create mode 100644 novaconium/vendor/twig/src/Sandbox/SecurityNotAllowedTagError.php create mode 100644 novaconium/vendor/twig/src/Sandbox/SecurityNotAllowedTestError.php create mode 100644 novaconium/vendor/twig/src/Sandbox/SecurityPolicy.php create mode 100644 novaconium/vendor/twig/src/Sandbox/SecurityPolicyInterface.php create mode 100644 novaconium/vendor/twig/src/Sandbox/SourcePolicyInterface.php create mode 100644 novaconium/vendor/twig/src/Source.php create mode 100644 novaconium/vendor/twig/src/Template.php create mode 100644 novaconium/vendor/twig/src/TemplateWrapper.php create mode 100644 novaconium/vendor/twig/src/Test/IntegrationTestCase.php create mode 100644 novaconium/vendor/twig/src/Test/NodeTestCase.php create mode 100644 novaconium/vendor/twig/src/Token.php create mode 100644 novaconium/vendor/twig/src/TokenParser/AbstractTokenParser.php create mode 100644 novaconium/vendor/twig/src/TokenParser/ApplyTokenParser.php create mode 100644 novaconium/vendor/twig/src/TokenParser/AutoEscapeTokenParser.php create mode 100644 novaconium/vendor/twig/src/TokenParser/BlockTokenParser.php create mode 100644 novaconium/vendor/twig/src/TokenParser/DeprecatedTokenParser.php create mode 100644 novaconium/vendor/twig/src/TokenParser/DoTokenParser.php create mode 100644 novaconium/vendor/twig/src/TokenParser/EmbedTokenParser.php create mode 100644 novaconium/vendor/twig/src/TokenParser/ExtendsTokenParser.php create mode 100644 novaconium/vendor/twig/src/TokenParser/FlushTokenParser.php create mode 100644 novaconium/vendor/twig/src/TokenParser/ForTokenParser.php create mode 100644 novaconium/vendor/twig/src/TokenParser/FromTokenParser.php create mode 100644 novaconium/vendor/twig/src/TokenParser/GuardTokenParser.php create mode 100644 novaconium/vendor/twig/src/TokenParser/IfTokenParser.php create mode 100644 novaconium/vendor/twig/src/TokenParser/ImportTokenParser.php create mode 100644 novaconium/vendor/twig/src/TokenParser/IncludeTokenParser.php create mode 100644 novaconium/vendor/twig/src/TokenParser/MacroTokenParser.php create mode 100644 novaconium/vendor/twig/src/TokenParser/SandboxTokenParser.php create mode 100644 novaconium/vendor/twig/src/TokenParser/SetTokenParser.php create mode 100644 novaconium/vendor/twig/src/TokenParser/TokenParserInterface.php create mode 100644 novaconium/vendor/twig/src/TokenParser/TypesTokenParser.php create mode 100644 novaconium/vendor/twig/src/TokenParser/UseTokenParser.php create mode 100644 novaconium/vendor/twig/src/TokenParser/WithTokenParser.php create mode 100644 novaconium/vendor/twig/src/TokenStream.php create mode 100644 novaconium/vendor/twig/src/TwigCallableInterface.php create mode 100644 novaconium/vendor/twig/src/TwigFilter.php create mode 100644 novaconium/vendor/twig/src/TwigFunction.php create mode 100644 novaconium/vendor/twig/src/TwigTest.php create mode 100644 novaconium/vendor/twig/src/Util/CallableArgumentsExtractor.php create mode 100644 novaconium/vendor/twig/src/Util/CallableParameters.php create mode 100644 novaconium/vendor/twig/src/Util/DeprecationCollector.php create mode 100644 novaconium/vendor/twig/src/Util/ReflectionCallable.php create mode 100644 novaconium/vendor/twig/src/Util/TemplateDirIterator.php create mode 100644 public/.htaccess create mode 100644 public/css/main.css create mode 100644 public/index.php create mode 100644 public/router.php delete mode 100644 sass/Dockerfile delete mode 100644 sass/abstracts/_variables.sass delete mode 100644 sass/abstracts/index.sass delete mode 100644 sass/base/_background.sass delete mode 100644 sass/base/_reset.sass delete mode 100644 sass/base/index.sass delete mode 100644 sass/coming-soon/index.sass delete mode 100644 sass/controlPanel/_footer.sass delete mode 100644 sass/controlPanel/_header.sass delete mode 100644 sass/controlPanel/_main.sass delete mode 100644 sass/controlPanel/_menu.sass delete mode 100644 sass/controlPanel/_panel.sass delete mode 100644 sass/controlPanel/index.sass delete mode 100644 sass/framework/_ace.sass delete mode 100644 sass/framework/_edit_page.sass delete mode 100644 sass/framework/_forms.sass delete mode 100644 sass/framework/_login_form.sass delete mode 100644 sass/framework/_logo.sass delete mode 100644 sass/framework/_main.sass delete mode 100644 sass/framework/_tabs.sass delete mode 100644 sass/framework/_tags.sass delete mode 100644 sass/framework/_tooltip.sass delete mode 100644 sass/framework/_ui.sass delete mode 100644 sass/framework/index.sass delete mode 100644 sass/novaconium.sass delete mode 100644 skeleton/.env delete mode 100644 skeleton/.gitignore delete mode 100644 skeleton/docker-compose.yml delete mode 100644 skeleton/novaconium/App/config.php delete mode 100644 skeleton/novaconium/App/controllers/humans.php delete mode 100644 skeleton/novaconium/App/controllers/index.php delete mode 100644 skeleton/novaconium/App/controllers/page.php delete mode 100644 skeleton/novaconium/App/controllers/robots.php delete mode 100644 skeleton/novaconium/App/routes.php delete mode 100644 skeleton/novaconium/App/templates/override.html.twig delete mode 100644 skeleton/novaconium/App/views/index.html.twig delete mode 100644 skeleton/novaconium/App/views/page.html.twig delete mode 100644 skeleton/novaconium/public/.htaccess delete mode 100644 skeleton/novaconium/public/css/novaconium.css delete mode 100644 skeleton/novaconium/public/index.php delete mode 100644 skeleton/novaconium/public/js/tabs.js delete mode 100644 skeleton/novaconium/sass/project.sass delete mode 100644 src/Database.php delete mode 100644 src/Logger.php delete mode 100644 src/MessageHandler.php delete mode 100644 src/Post.php delete mode 100644 src/Redirect.php delete mode 100644 src/Router.php delete mode 100644 src/Services/Auth.php delete mode 100644 src/Services/TagManager.php delete mode 100644 src/Session.php delete mode 100644 src/functions.php delete mode 100644 src/novaconium.php delete mode 100644 src/twig.php delete mode 100644 twig/coming-soon/index.html.twig delete mode 100644 twig/cp/control-panel.html.twig delete mode 100644 twig/cp/head.html.twig delete mode 100644 twig/cp/menu.html.twig delete mode 100644 twig/foot.html.twig delete mode 100644 twig/footer.html.twig delete mode 100644 twig/head.html.twig delete mode 100644 twig/javascript/ace.html.twig delete mode 100644 twig/javascript/page-edit.html.twig delete mode 100644 twig/left.html.twig delete mode 100644 twig/master.html.twig delete mode 100644 twig/nav.html.twig delete mode 100644 twig/right.html.twig delete mode 100644 views/404.html.twig delete mode 100644 views/auth/login.html.twig delete mode 100644 views/coming-soon.html.twig delete mode 100644 views/dashboard.html.twig delete mode 100644 views/editmessage.html.twig delete mode 100644 views/editpage/index.html.twig delete mode 100644 views/editpage/tab-main.html.twig delete mode 100644 views/editpage/tab-metadata.html.twig delete mode 100644 views/editpage/tab-notes.html.twig delete mode 100644 views/editpage/tab-other.html.twig delete mode 100644 views/editpage/tab-tags.html.twig delete mode 100644 views/editpage/tab-tweaks.html.twig delete mode 100644 views/init.html.twig delete mode 100644 views/messages.html.twig delete mode 100644 views/pages.html.twig delete mode 100644 views/samples/basic.html.twig delete mode 100644 views/settings.html.twig diff --git a/.gitignore b/.gitignore index 4c5f206..58b6df3 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,4 @@ +/public/cache/* +!/public/cache/.gitkeep +/novaconium/contact-log.txt .claude/ diff --git a/AGENTS.md b/AGENTS.md index 340340c..cc594b3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,59 +1,240 @@ -# NovaconiumPHP +# AGENTS.md -A lightweight PHP 8.1+ MVC-ish web framework/CMS toolkit (router + controllers + Twig views + Sass styling), authored by Nick Yeoman (4lt.ca) and distributed via Composer as `4lt/novaconium`. The canonical repo is hosted on a self-hosted Gitea instance (`git.4lt.ca`), not GitHub — do not assume `gh` CLI or GitHub Actions apply here. +Context for any coding agent working in this repo — Claude, DeepSeek, or +otherwise; this file (and the maintenance rule below) applies regardless of +which model or CLI is driving. Full narrative docs live at `/admin/docs` +when the app is running (also the *only* place Twig upgrade instructions +live now — see `/admin/docs/upgrading-twig`; there's no separate +MAINTENANCE.md, keeping one copy in the docs page avoids drift). `README.md` +is the GitHub-facing pitch, `novaconium/ISSUES.md` is the roadmap/backlog, +and this file is the short, agent-facing version. The original design +rationale used to live in a standalone `plan.md`; it's now folded into +`/admin/docs/design-notes` (everything in it shipped) and the file was +deleted. There used to also be a +`GUIDE.md` mirroring `/admin/docs` for offline reading — it was removed to +cut a doc copy that had to be kept in sync; `/admin/docs` is the only +narrative reference now. -## Directory map +## Documentation is duplicated on purpose — keep all copies in sync -- `src/` — core framework classes, namespace `Novaconium\` (PSR-4 → `src/`): `novaconium.php` (bootstrap), `Router.php`, `Database.php`, `Session.php`, `Redirect.php`, `Post.php`, `Logger.php`, `MessageHandler.php`, `functions.php`, `twig.php`, and `src/Services/` (`Auth.php`, `TagManager.php`). -- `controllers/` — framework-level default controllers (dashboard, pages, messages, settings, auth/, sitemap, 404, init, create_admin, etc.). -- `config/routes.php` — framework-level default route definitions, merged with app-level `App/routes.php` at runtime. -- `views/` — Twig views mirroring controller names (`controllers/dashboard.php` ↔ `views/dashboard.html.twig`). -- `twig/` — shared/partial Twig templates (layout pieces: `master.html.twig`, `nav.html.twig`, `head.html.twig`, `foot.html.twig`, `cp/` control-panel partials, `javascript/`). -- `sass/` — framework's default Sass source (indented Sass, not SCSS), 7-1-ish structure: `abstracts/`, `base/`, `framework/`, `controlPanel/`, `coming-soon/`; has its own `Dockerfile` for compiling. -- `skeleton/` — scaffold copied into a new consumer project on install: `.env`, `docker-compose.yml`, `novaconium/App/{config.php,routes.php,controllers,views,templates}`, `novaconium/public/{index.php,.htaccess,css,js}`, `novaconium/sass/project.sass`. -- `docs/` — one short Markdown file per topic (see below). -- `_assets/` — static assets (e.g. logo used in README). +Every topic (routing, sidecars, libraries, layouts, caching, SEO, Matomo, +admin authentication, styling, project layout, third-party) exists in +**two** places: a page under `novaconium/pages/admin/docs//index.twig` +(the canonical reference), and (for anything a README-reading human needs +up front) a mention in `README.md`. This is intentional — `/admin/docs` is +for reading against a running instance with no internet needed, and +`README.md` is the GitHub-facing pitch — but it means **any agent that +changes framework behavior or adds a feature must update both copies in +the same change**, not just the one that was open. Concretely, after +touching routing/rendering/caching/SEO behavior or adding a new top-level +docs topic: -## Build / run (Docker-only — no host tooling assumed) +1. Update (or add) the matching page under + `novaconium/pages/admin/docs//index.twig`, and if it's a new + topic, link it from both `admin/docs/index.twig` and the nav in + `admin/docs/_layout/layout.twig`. +2. Update `README.md` if the change affects the feature list, getting + started steps, or the docs index there. +3. Update this file if the change affects a convention an agent needs to + know before editing code (not just narrative docs). -**Agents (Claude Code, opencode, etc.) run inside containers and have no access to Docker.** Do not attempt to run any `docker`/`docker compose` command below — they will fail. These commands are documented for a human maintainer to run on their own host. If you need to verify a change, do so by reading/reasoning about the code, not by invoking Docker. +A doc change that only touches one of these copies is incomplete — +verify the other copy before considering the task done. + +## What this is + +A dependency-light PHP + Twig micro-framework: directories under `pages/` +map directly to URLs (Hugo-style page bundles), optional `index.php` +sidecars supply data or short-circuit to a `Response`, and sidecar-less +pages get pre-rendered to static HTML on first request and served straight +from Apache after that. No Composer, no build step to install — Twig is +vendored as plain source files. + +## The two-root split — read this before editing anything under `pages/` or `lib/` + +Everything lives in one of two places: + +- **`App/`** — the actual project: `App/pages/` (routes/content) and + `App/lib/` (project's own `Lib\` classes). This is the only directory a + site author is expected to touch. +- **`novaconium/`** — the framework itself: router/renderer core + (`novaconium/src/`), default pages (`novaconium/pages/` — root layout, + 404, the `/admin` tools), default `Lib\` classes (`novaconium/lib/`), + vendored Twig, autoloader, config, bootstrap. + +Routing and rendering resolve against **both roots, in order** — +`App/pages/` first, `novaconium/pages/` as fallback — via +`novaconium/src/Overlay.php`. Same mechanism for `Lib\` classes: +`App/lib/` is checked before `novaconium/lib/` in `novaconium/autoload.php`. +Concretely: dropping a file at the same relative path in `App/` overrides +the `novaconium/` default; nothing needs to be duplicated for the site to +work, since `novaconium/pages/` already supplies a working layout and 404. + +Twig's `FilesystemLoader` is constructed with both paths as an array, so +`{% extends %}` / `{% include %}` get this override-then-fallback +resolution for free — no custom logic needed there. + +The same override-by-presence pattern applies to `novaconium/config.php`: +if `App/config.php` exists, `novaconium/bootstrap.php` and +`novaconium/bin/clear-cache.php` shallow-merge it over the framework defaults +with `array_merge()`. A project only needs to list the keys it's changing +— never edit `novaconium/config.php` directly. + +`config['matomo_url']` / `config['matomo_site_id']` (both default `''`) +gate the Matomo tracking script emitted by the root layout — set both via +`App/config.php` to enable it, since either being empty disables tracking +entirely. `bootstrap.php` normalizes a missing trailing slash on +`matomo_url` before passing it to `Renderer`, which exposes `matomo_url`, +`matomo_site_id`, and `is_404` as Twig globals (`is_404` is overridden to +`true` in the 404 template's local render context by +`Renderer::renderNotFound()`, per Twig's local-context-over-global +precedence). Any new Twig global added to `Renderer`'s constructor should +follow this same pattern: default value, `addGlobal()` call, documented +here and in `/admin/docs`. + +`config['site_name']` (default `'My Site'`) is the same pattern — passed to +`Renderer` and exposed as the `site_name` Twig global, used by +`novaconium/pages/_layout/layout.twig` for the default `title` block, +`og:site_name`, and the footer copyright line. Any other hardcoded +site-identity string that shows up in a shared template (as opposed to a +per-page override) should become a `config.php` key the same way, not stay +hardcoded in the template. + +`config['admin_username']` / `config['admin_password_hash']` (username +defaults to `'admin'`, password hash defaults to `''`) gate every +`/admin/*` route behind HTTP Basic Auth — this replaced the old +`docs_enabled` flag entirely (removed); a single gate over all of +`/admin/*` (docs included) made a docs-only toggle redundant. Unlike the +Twig-global pattern above, the gate itself is enforced in `bootstrap.php`, +before rendering: `AdminAuth::requireLogin(...)` +(`novaconium/src/AdminAuth.php`) is called once for any resolved route +whose path is `admin` or starts with `admin/`. **Any new admin page +dropped under `App/pages/admin/` or `novaconium/pages/admin/` is +automatically protected — no per-page wiring needed.** `bootstrap.php` +also special-cases the literal path `/admin/logout` *before* router +resolution — no page exists there — to call `AdminAuth::logout()`, which +always issues a fresh 401 so the browser drops its cached credentials +(there's no server-side session to invalidate). `Renderer` separately +exposes an `admin_auth_enabled` Twig global (true when a password hash is +set) so `admin/index.twig` can conditionally show the "Logout" link — this +is a derived display flag, not the enforcement mechanism itself, which +never depends on Twig. `novaconium/pages/admin/password-hash/` is a +built-in `password_hash()` form (no CLI needed) for generating +`admin_password_hash` — a normal admin page, so it's covered by the same +gate: reachable while no password is set yet (to generate the first +one), then protected like everything else under `/admin/*` afterward. It +computes and displays the hash per-request only; nothing is persisted or +logged. This is a single-user HTTP Basic Auth stopgap, not +the full multi-user system tracked in `novaconium/ISSUES.md` ("Admin login & user +management"); don't extend this class toward multi-user/session-based +auth — that's a separate, larger feature that +will replace it. + +## Running it -**Composer** (per `docs/Composer.md`): ``` -docker run --rm --interactive --tty --volume $PWD:/app composer:latest require 4lt/novaconium -docker run --rm --interactive --tty --volume $PWD:/app composer:latest update +php -S 127.0.0.1:8000 -t public public/router.php ``` -**Sass build** (per `docs/Sass.md`), using `sass/Dockerfile`: -``` -cd sass && docker build -t sass-container . -docker run --rm -v $(pwd):/usr/src/app -w /usr/src/app sass-container sass novaconium/sass/project.sass novaconium/public/css/novaconium.css -``` -Compressed variant: add `--style=compressed`, e.g. -``` -docker run --rm -v $(pwd):/usr/src/app -w /usr/src/app sass-container --style=compressed sass/novaconium.sass skeleton/novaconium/public/css/novaconium.css -``` +`public/router.php` is dev-only, mimics `public/.htaccess`. There is no +test suite — verification is manual route-by-route (see +`/admin/docs/design-notes`'s Verification section for the checklist used +after any framework change). +After testing, clear stray cache with `php novaconium/bin/clear-cache.php` or +POST `/admin/clear-cache`, and remove anything written to `App/lib/` or +`App/pages/` that was only for testing an override — nothing here is +gitignored except `public/cache/*` and `novaconium/contact-log.txt`, so +test debris left in `App/` will otherwise get committed or silently change +site behavior for the next person. -**Dev stack**: `docker compose up -d` from `skeleton/docker-compose.yml` — services: `4lights/corxn:8.5.3` (Apache+PHP), `redis`, `mariadb`, `phpmyadmin`. +## Conventions worth knowing -**Testing a cloned copy without reinstalling via Composer**: see `docs/Dev-Fake_autoload.md` for the fake-autoload dev trick. - -## No test suite / lint / CI - -There is no `phpunit.xml`, no lint config (no PHPCS, `.editorconfig`, ESLint), and no CI pipeline in this repo. Don't hunt for `npm test`/`phpunit`/lint commands — none exist. - -## Conventions - -- PSR-4 autoloading: `Novaconium\` → `src/`. -- Controllers and views are name-paired by default (e.g. `controllers/dashboard.php` ↔ `views/dashboard.html.twig`), but this is only a convention — a controller can render any view, they are not required to match. -- Sass partials are prefixed `_` (e.g. `_forms.sass`), aggregated per-folder via `index.sass`. -- App-level config (`App/config.php`) is a multi-dimensional PHP array: database credentials, `base_url`, `secure_key`, `logfile`, `loglevel`. -- Versioning strategy is semantic-versioning (declared in `composer.json` `extra.versioning`). - -## Git workflow - -Single `master` branch (no `main`), tracking a self-hosted Gitea remote. Solo-maintainer, trunk-based, direct commits to `master` with short informal messages — no conventional-commits enforcement, no PR-based tooling assumptions. - -## Further reading - -See `docs/*.md` for per-topic detail: `404.md`, `Composer.md`, `ConfigurationFile.md`, `Dev-Fake_autoload.md`, `Logs.md`, `Messages.md`, `Post.md`, `Redirect.md`, `Sass.md`, `Session.md`, `StyleSheets-sass.md`, `Twig-Views.md`, `docker.md`. +- Reserved segments: any path segment starting with `_` (e.g. `_layout/`) + or literally named `404` is never routable — `Router::resolve()` 404s on + sight, don't try to serve content directly at those paths. +- Sidecars (`index.php`) return either an array (Twig context) or a + `Response` (redirect/json/xml/html — `novaconium/src/Response.php`). + `$params` (route captures) and `$cache` (the `Cache` instance, e.g. for + `$cache->clear()`) are both in scope automatically — see + `novaconium/src/Renderer.php::runSidecar()`. +- No Composer — `novaconium/autoload.php` is a hand-rolled PSR-4 loader. + Adding a new framework-core class means adding it under `App\` in + `novaconium/src/`; a new `Lib\` class goes in `App/lib/` or + `novaconium/lib/` depending on whether it's project- or + framework-specific. +- `novaconium/bin/` holds standalone CLI entry points meant to be run + directly (`php novaconium/bin/ +{% endif %} diff --git a/novaconium/pages/_layout/nav.twig b/novaconium/pages/_layout/nav.twig new file mode 100644 index 0000000..691d57c --- /dev/null +++ b/novaconium/pages/_layout/nav.twig @@ -0,0 +1,25 @@ +{% import '_layout/icons.twig' as icons %} + + diff --git a/novaconium/pages/_layout/theme-init.twig b/novaconium/pages/_layout/theme-init.twig new file mode 100644 index 0000000..43bee4e --- /dev/null +++ b/novaconium/pages/_layout/theme-init.twig @@ -0,0 +1,12 @@ +{# Applies a saved theme choice before the page paints, so switching to + light doesn't flash dark first. Must run early in , before the + stylesheet link — see novaconium/pages/_layout/layout.twig. Pairs with + the toggle button + its click handler in novaconium/pages/_layout/nav.twig. #} + diff --git a/novaconium/pages/admin/clear-cache/index.php b/novaconium/pages/admin/clear-cache/index.php new file mode 100644 index 0000000..0bb2086 --- /dev/null +++ b/novaconium/pages/admin/clear-cache/index.php @@ -0,0 +1,22 @@ +clear(); + + return Response::redirect('/admin/clear-cache?cleared=1'); +} + +return [ + 'cleared' => Input::get('cleared') !== null, + 'securityError' => Input::get('error') === 'security', + 'csrfField' => Csrf::fieldName(), + 'csrfToken' => Csrf::token(), +]; diff --git a/novaconium/pages/admin/clear-cache/index.twig b/novaconium/pages/admin/clear-cache/index.twig new file mode 100644 index 0000000..42cdfe5 --- /dev/null +++ b/novaconium/pages/admin/clear-cache/index.twig @@ -0,0 +1,26 @@ +{% extends layout %} + +{% import '_layout/icons.twig' as icons %} + +{% block title %}Clear cache{% endblock %} + +{% block description %}Clear the static page cache.{% endblock %} + +{% block robots %}noindex, nofollow{% endblock %} + +{% block content %} +
+

{{ icons.trash() }}Clear cache

+

Deletes every pre-rendered page under public/cache/. Sidecar-less pages re-render (and re-cache) on their next request.

+ {% if cleared %} +

Cache cleared.

+ {% endif %} + {% if securityError %} +

Your session expired before submitting — please try again.

+ {% endif %} +
+ + +
+
+{% endblock %} diff --git a/novaconium/pages/admin/docs/_layout/layout.twig b/novaconium/pages/admin/docs/_layout/layout.twig new file mode 100644 index 0000000..fe14aad --- /dev/null +++ b/novaconium/pages/admin/docs/_layout/layout.twig @@ -0,0 +1,32 @@ +{% extends '_layout/layout.twig' %} + +{% import '_layout/icons.twig' as icons %} + +{% block content %} + +{% endblock %} diff --git a/novaconium/pages/admin/docs/admin-auth/index.twig b/novaconium/pages/admin/docs/admin-auth/index.twig new file mode 100644 index 0000000..9639303 --- /dev/null +++ b/novaconium/pages/admin/docs/admin-auth/index.twig @@ -0,0 +1,50 @@ +{% extends 'admin/docs/_layout/layout.twig' %} + +{% import '_layout/icons.twig' as icons %} + +{% block title %}Admin authentication{% endblock %} + +{% block description %}Gating /admin/* behind HTTP Basic Auth, reusable for any future admin page.{% endblock %} + +{% block robots %}noindex, nofollow{% endblock %} + +{% block docs_content %} +

Admin authentication

+ +

Every route under /admin/*/admin, /admin/clear-cache, the docs section, and any admin page a project adds later — can be gated behind HTTP Basic Auth with two App/config.php keys. It's off by default (same posture as Matomo): an empty admin_password_hash means no gate at all, matching this project's original wide-open behavior.

+ +

This replaced the old docs_enabled config flag, which only hid the docs section specifically. Since this gate covers all of /admin/* — including docs — there's no need for a separate docs-only toggle anymore; set a password and the whole admin area (docs included) requires login.

+ +

Enabling it

+ +

Generate a password hash once — either on the command line:

+ +
php -r "echo password_hash('yourpassword', PASSWORD_DEFAULT), PHP_EOL;"
+ +

...or, if you'd rather not touch a terminal, at {{ icons.lock() }}/admin/password-hash — a small built-in form that does the same password_hash() call and hands back a ready-to-paste config snippet. Nothing typed there is stored or logged. Since it lives under /admin/* like everything else here, it's automatically covered by this same gate once a password is set — reachable while admin_password_hash is still empty (so you can generate your first one), then protected like any other admin page afterward.

+ +

Then set both keys in App/config.php:

+ +
<?php
+// App/config.php
+return [
+    'admin_username'      => 'admin',
+    'admin_password_hash' => '$2y$10$...',
+];
+ +

Every request into /admin/* now requires that username/password via the browser's built-in Basic Auth prompt; anything outside /admin is unaffected.

+ +

How it's wired

+ +

novaconium/src/AdminAuth.php is a single reusable check — AdminAuth::requireLogin($username, $passwordHash) — called once from novaconium/bootstrap.php for any resolved route whose path is admin or starts with admin/, and only for routes that actually resolved (no login prompt on an unrelated 404). Because the check lives in bootstrap.php rather than on each page, a new admin page needs zero extra wiring to be protected — dropping a new directory under App/pages/admin/ or novaconium/pages/admin/ is automatically gated the moment it exists.

+ +

Logging out

+ +

HTTP Basic Auth has no real server-side logout — the browser just keeps resending the same cached credentials on every request to that realm. Visiting /admin/logout works around this: AdminAuth::logout() always issues a fresh 401 challenge, regardless of what credentials were sent, which makes the browser discard what it had cached and prompt again the next time /admin is visited. Credentials themselves aren't invalidated server-side (there's nothing to invalidate — it's just a password check on every request), so this is a client-side-only logout, same as any Basic Auth site. The "Logout" link only appears on /admin when admin_auth_enabled is true (i.e. a password is actually set).

+ +

What this is (and isn't)

+ +

This is HTTP Basic Auth against a single username/password pair in config — no sessions, no user table, no password reset, no multiple accounts. It's a deliberate stopgap: see novaconium/ISSUES.md's "Admin login & user management" entry for the planned real multi-user system (backed by SQLite, with proper sessions). That feature will replace this mechanism, not layer on top of it. Until then, this is enough to keep the general public out of /admin on a production site.

+ +

Basic Auth credentials are sent base64-encoded on every request (not encrypted) — always serve /admin over HTTPS in production, same as any password-protected page.

+{% endblock %} diff --git a/novaconium/pages/admin/docs/caching/index.twig b/novaconium/pages/admin/docs/caching/index.twig new file mode 100644 index 0000000..af67f41 --- /dev/null +++ b/novaconium/pages/admin/docs/caching/index.twig @@ -0,0 +1,22 @@ +{% extends 'admin/docs/_layout/layout.twig' %} + +{% block title %}Static caching{% endblock %} + +{% block description %}How sidecar-less pages are pre-rendered and served as static HTML.{% endblock %} + +{% block robots %}noindex, nofollow{% endblock %} + +{% block docs_content %} +

Static caching

+ +

If a page has no sidecar, its rendered HTML is written to public/cache/<path>/index.html after the first request. .htaccess checks for that file before PHP ever runs, so repeat visits are served straight by Apache with zero PHP/Twig overhead. Pages with a sidecar are never cached this way, since their output can vary per request.

+ +

To force a single page to re-render, delete its file under public/cache/. To clear everything at once, there are two equivalent options:

+ +
    +
  • CLI: php novaconium/bin/clear-cache.php — a standalone script for deploys, cron jobs, or anywhere you'd rather not go through a browser. Prints Cache cleared. and exits.
  • +
  • Web: /admin/clear-cache — a POST form under /admin, covered by the same HTTP Basic Auth gate as the rest of /admin/* once a password is set.
  • +
+ +

Both end up calling the same underlying Cache::clear() — see /admin/docs/config's "For developers: using Cache.php directly" section for how each entry point constructs it. Clearing the cache only deletes the generated static HTML; it doesn't affect App/pages/ or any other source. Any project change that should show up on an already-cached page — a new site_name, a new Sass color, a new admin toggle — needs a cache clear before it's visible, since the old index.html would otherwise keep being served as-is.

+{% endblock %} diff --git a/novaconium/pages/admin/docs/config/index.twig b/novaconium/pages/admin/docs/config/index.twig new file mode 100644 index 0000000..827ab64 --- /dev/null +++ b/novaconium/pages/admin/docs/config/index.twig @@ -0,0 +1,71 @@ +{% extends 'admin/docs/_layout/layout.twig' %} + +{% block title %}Configuration{% endblock %} + +{% block description %}How to override framework settings without editing novaconium/config.php.{% endblock %} + +{% block robots %}noindex, nofollow{% endblock %} + +{% block docs_content %} +

Configuration

+ +

novaconium/config.php holds the framework defaults — pages_dirs, cache_dir, debug, site_name, matomo_url, matomo_site_id, admin_username, admin_password_hash — and is not meant to be edited per-project, same as everything else under novaconium/.

+ +

App/config.php ships with the skeleton as an empty, commented placeholder — uncomment (or add) whichever keys you want to change, returning an array of just those:

+ +
<?php
+// App/config.php
+return [
+    'debug' => false,
+];
+ +

novaconium/bootstrap.php (and novaconium/bin/clear-cache.php) check whether App/config.php exists and, if so, shallow-merge it over novaconium/config.php's defaults with array_merge() — the same App-overrides-novaconium pattern used for pages and Lib\ classes elsewhere. You only need to list the keys you're changing; anything you omit keeps the framework default.

+ +

Deleting App/config.php entirely is just as valid as leaving it in place returning an empty array — either way, every setting falls back to novaconium/config.php's defaults.

+ +

Site name

+ +

site_name (default 'My Site') is used by the root layout as the default page <title> (when a page doesn't override the title block), og:site_name, and the footer copyright line:

+ +
<?php
+// App/config.php
+return [
+    'site_name' => 'Nick Yeoman',
+];
+ +

See SEO for the full list of overridable meta blocks. Pages that were already statically cached before this changes need php novaconium/bin/clear-cache.php to pick it up.

+ +

Admin authentication

+ +

admin_username / admin_password_hash gate every /admin/* route behind HTTP Basic Auth — see Admin authentication for the full write-up. Both are set via App/config.php; leaving admin_password_hash empty (the default) disables the gate.

+ +

For developers: using Cache.php directly

+ +

novaconium/src/Cache.php is the class behind the cache_dir config key above — a small, dependency-free wrapper around writing/deleting the static HTML files under public/cache/ that Static caching describes. Like Router (see /admin/docs/routing's "For developers" section), it's plain and easy to reason about in isolation: no Twig, no request state, just a path convention and some filesystem calls.

+ +

How it fits in

+ +

Cache shows up in three places, all constructed the same way — new Cache($config['cache_dir']):

+ +
    +
  • novaconium/bootstrap.php constructs one and hands it to Renderer, which calls $cache->write() after rendering any page that has no sidecar (see /admin/docs/sidecars's Renderer.php section) — Renderer is the only thing that ever writes to the cache.
  • +
  • novaconium/bin/clear-cache.php, a standalone CLI script, constructs one and calls $cache->clear() — this is what php novaconium/bin/clear-cache.php runs.
  • +
  • novaconium/pages/admin/clear-cache/index.php's sidecar calls $cache->clear() too, but doesn't construct it — $cache is already in scope automatically inside every sidecar, the same instance Renderer is using, injected by Renderer::runSidecar() alongside $params.
  • +
+ +

Constructing it and its three methods

+ +
use App\Cache;
+
+$cache = new Cache($config['cache_dir']);
+
+$cache->path($requestUri);     // string  — the .html file a URI maps to, without touching the filesystem
+$cache->write($requestUri, $html); // void — creates parent directories as needed, then writes the file
+$cache->clear();               // void  — recursively deletes everything under cache_dir, leaving the directory itself
+ +

The constructor takes just one thing — cache_dir, a single absolute path (novaconium/config.php sets it to public/cache) — unlike Router/Renderer, which take the whole ordered pagesDirs list. That's because caching isn't part of the App-over-novaconium override mechanism; there's exactly one cache directory, not a searched list of them.

+ +

path() mirrors the public URL tree directly: /blog/hello-world maps to {cache_dir}/blog/hello-world/index.html, matching the .htaccess rule that checks for that exact file before letting any request reach PHP. write() calls path() internally and creates any missing parent directories with mkdir(..., true) before writing. clear() recurses over every entry under cache_dir deleting files and subdirectories, but never deletes cache_dir itself — so a stray public/cache/.gitkeep (see .gitignore) survives a clear.

+ +

Because every method here is a straightforward filesystem operation with no hidden state beyond the one constructor argument, testing Cache in isolation is just a matter of pointing it at a temporary directory and asserting on what ends up on disk.

+{% endblock %} diff --git a/novaconium/pages/admin/docs/design-notes/index.twig b/novaconium/pages/admin/docs/design-notes/index.twig new file mode 100644 index 0000000..fc37724 --- /dev/null +++ b/novaconium/pages/admin/docs/design-notes/index.twig @@ -0,0 +1,84 @@ +{% extends 'admin/docs/_layout/layout.twig' %} + +{% block title %}Design notes{% endblock %} + +{% block description %}The original design rationale for this framework.{% endblock %} + +{% block robots %}noindex, nofollow{% endblock %} + +{% block docs_content %} +

File-based PHP + Twig micro-router (Hugo-style, static-cached, Apache)

+ +

Context

+

User likes Hugo's file-based routing/pretty-URLs but wants real PHP capability and Twig instead of Markdown. Pages are files on disk (page-bundle style): a directory tree of .twig templates defines the URL structure, and any page needing PHP logic gets an optional sidecar index.php that supplies data or a full custom response (redirect/JSON/XML). Pages without a sidecar are pure/deterministic, so their rendered output is cached to a static .html file and served directly by Apache — PHP is skipped entirely on repeat hits, similar to Hugo's static output but generated lazily on first request instead of an upfront build. Deployment target is Apache only, so .htaccess handles both the cache short-circuit and canonical-URL redirects.

+ +

Pages/layouts are overridable, same mechanism as Lib\ overrides

+

Routing and rendering key off an ordered list of roots (App/pages/ then novaconium/pages/) and resolve every relative path (a page, a sidecar, a _layout/layout.twig) against that list via Overlay::isFile/isDir/findFile/findWildcardDir, first match wins. novaconium/pages/ ships the framework defaults (_layout/layout.twig, 404/index.twig); a project doesn't need to duplicate those to have a working site, but overrides either — or any page — just by placing a file at the same relative path under App/pages/. Twig's FilesystemLoader natively accepts an array of paths, so template rendering (including {% verbatim %}{% extends %}{% endverbatim %}) gets the same override-then-fallback resolution for free — no manual proxying required there.

+ +

Router (novaconium/src/Router.php) — intentionally simple, no rendering

+
    +
  1. Take $_SERVER['REQUEST_URI'], strip query string, trim slashes, split into segments (root = []).
  2. +
  3. Walk the page roots via Overlay, skipping any directory starting with _ when matching (those are reserved, e.g. _layout/, never routable). At each level: exact-name subdirectory first (found in any root), else a [param]-named subdirectory (capturing into params[name]), else no match.
  4. +
  5. Returns a Route object: { dir: string|null (relative path, not absolute), params: array, found: bool }. Never touches Twig, sidecars, or output — that's the Renderer's job. This keeps Router a pure "URL -> relative page path" lookup, easy to unit-test on its own.
  6. +
+ +

Sidecar PHP contract — can return more than an array

+

App/pages/**/index.php (optional) returns one of:

+
    +
  • array → treated as Twig context data ($params is in scope for slug etc.).
  • +
  • Response object → short-circuits Twig entirely. novaconium/src/Response.php provides factories: Response::redirect($url, $code = 301), Response::json($data), Response::xml($string), Response::html($string) (raw HTML, bypass Twig but still gets cached like a normal page if desired).
  • +
+

Renderer checks the sidecar's return type: array → render matched index.twig with that data; Response → emit directly (correct headers/status, no Twig, no layout).

+ +

Layout inheritance — walk upward like Hugo

+

Reserved _layout/layout.twig directories are looked up by the Renderer, not baked into Router:

+
    +
  1. Starting at the matched page's directory, check for _layout/layout.twig via Overlay (checking App/pages/ before novaconium/pages/ at every level). If absent, check parent directory, and so on up to the root _layout/layout.twig (App/pages/_layout/layout.twig if present, else novaconium/pages/_layout/layout.twig).
  2. +
  3. The resolved layout path is injected into the Twig context as layout, and each index.twig does {% verbatim %}{% extends layout %}{% endverbatim %} — keeps the mechanism transparent in the template rather than magic in the engine.
  4. +
+ +

Static caching (sidecar-less pages only)

+
    +
  1. When Renderer serves a page whose directory has no index.php, after rendering the Twig output it also writes it to public/cache/<segments>/index.html.
  2. +
  3. .htaccess checks public/cache/<REQUEST_URI>/index.html first (RewriteCond ... -f) and serves it directly if present — PHP/Twig never runs again for that route until the cache file is cleared.
  4. +
  5. Cache invalidation is manual for now: clearing public/cache/ (or a specific subfolder) forces regeneration on next hit. (Simple, matches "no build step" philosophy; can add mtime-based invalidation later if needed.)
  6. +
  7. Pages with a sidecar are never cached this way since their output can vary per-request (DB data, params, POST handling, etc.).
  8. +
+ +

Canonical URLs — no trailing slash (SEO)

+
    +
  • Canonical form is without a trailing slash: /about, /blog/hello-world.
  • +
  • .htaccess 301-redirects any URL ending in / (except the bare root /) to the same path without it, before any routing/cache logic runs.
  • +
  • Router/Renderer internally still key off directory-per-page (pages/about/index.twig), that's just the filesystem convention — it's independent of what the external canonical URL looks like.
  • +
+ +

Front controller (public/index.php) — kept intentionally light

+
<?php
+require __DIR__ . '/../novaconium/bootstrap.php';
+

All real logic — autoload registration, config load, Router::resolve(), Renderer::render(), emitting headers/body — lives in novaconium/bootstrap.php, not in public/index.php.

+ +

Sass (indented syntax, not SCSS)

+
    +
  • Source lives in novaconium/sass/ (structure in main.sass, default colors in defaults/_colors.sass) using indented syntax. App/sass/_colors.sass overrides the color palette.
  • +
  • Compiled output goes to public/css/main.css.
  • +
  • Compilation is a build step (not a PHP runtime concern) — use the sass CLI (Dart Sass, which supports indented syntax) e.g. sass --load-path=App/sass --load-path=novaconium/sass/defaults novaconium/sass/main.sass public/css/main.css. No PHP Sass library is needed/assumed since PHP-based compilers (scssphp) target SCSS, not indented syntax.
  • +
  • main.sass does @use 'colors' as * but its own directory has no _colors.sass — on purpose, so resolution falls through to the load paths above (App/sass checked first) instead of resolving to a same-directory sibling, which Dart Sass would otherwise prefer regardless of load-path order. Same override-by-presence mechanism used for pages/lib, extended to a non-PHP asset.
  • +
+ +

Autoloading (no Composer)

+

novaconium/autoload.php: spl_autoload_register mapping Twig\novaconium/vendor/twig/src/, App\novaconium/src/, and Lib\ → checked against App/lib/ first, falling back to novaconium/lib/ — this is the override mechanism: a project drops a same-named class in App/lib/ to replace a framework default without touching novaconium/. Twig vendored manually from a GitHub release zip into novaconium/vendor/twig/.

+ +

Verification

+
    +
  • Configure Apache (or equivalent local Apache setup) with docroot public/ and the .htaccess rules active (AllowOverride All / mod_rewrite enabled).
  • +
  • Visit /about/ → confirms 301 redirect to /about (canonical, no trailing slash).
  • +
  • Visit /about twice → first hit renders via PHP/Twig and writes public/cache/about/index.html; second hit confirms (e.g. via response headers/timing, or temporarily renaming novaconium/bootstrap.php) that Apache served the cached file directly.
  • +
  • Visit /contact (has a sidecar) → confirms it is NOT cached (no sidecar-less caching), and that a Lib\ class (Lib\Mailer) can be called from it. Every post under App/pages/blog/ is sidecar-less by contrast — visiting one twice should show the same static-cache behavior as /about above.
  • +
  • Add a sidecar that returns Response::json([...]) → confirms JSON is emitted with correct Content-Type, bypassing Twig.
  • +
  • Add a sidecar that returns Response::redirect('/somewhere') → confirms a real HTTP redirect happens.
  • +
  • Confirm App/pages/blog/_layout/layout.twig overrides App/pages/_layout/layout.twig for pages under blog/, and that _layout/ directories are never reachable as routes (404 if requested directly).
  • +
  • Drop a same-named class into App/lib/ and confirm it's used instead of the novaconium/lib/ default (override mechanism works).
  • +
  • Delete App/pages/_layout/ and App/pages/404/ (if present) and confirm the site still renders and 404s using novaconium/pages/'s defaults; then drop a _layout/layout.twig into App/pages/ and confirm it takes precedence.
  • +
  • Run sass --load-path=App/sass --load-path=novaconium/sass/defaults novaconium/sass/main.sass public/css/main.css and confirm compiled CSS loads on a page.
  • +
+{% endblock %} diff --git a/novaconium/pages/admin/docs/forms/index.twig b/novaconium/pages/admin/docs/forms/index.twig new file mode 100644 index 0000000..56146b0 --- /dev/null +++ b/novaconium/pages/admin/docs/forms/index.twig @@ -0,0 +1,137 @@ +{% extends 'admin/docs/_layout/layout.twig' %} + +{% import '_layout/icons.twig' as icons %} + +{% block title %}Forms{% endblock %} + +{% block description %}Building a custom form with a sidecar, using Lib\FormValidator, Lib\SpamGuard, and Lib\Validate.{% endblock %} + +{% block robots %}noindex, nofollow{% endblock %} + +{% block docs_content %} +

Forms

+ +

Every form on this site — the {{ icons.email() }}contact form included — is the same four ingredients: a page with a sidecar, the three validation/spam classes under Lib\, and the POST/redirect/GET pattern. This page builds a second, different form from scratch (a one-field newsletter signup) so the pattern is clear independent of the contact form's specific fields.

+ +

1. Create the page

+ +

A form needs a directory with both an index.twig and an index.php — the sidecar is what makes it a form rather than a static page. Scaffold the Twig half with novaconium/bin/create-static-page.php (see {{ icons.book() }}Getting started) and add the sidecar yourself, or just create both by hand:

+ +
App/pages/newsletter/
+    index.twig
+    index.php
+ +

2. The sidecar

+ +

This is the whole contract: validate $_POST, check for spam, do something with the result, redirect. Nothing here is specific to "newsletter" — swap the one field and the one line that does something with valid input, and this is the shape of any form on the site:

+ +
{% verbatim %}<?php
+// App/pages/newsletter/index.php
+
+use App\Response;
+use Lib\Csrf;
+use Lib\FormValidator;
+use Lib\Input;
+use Lib\SpamGuard;
+
+$errors = [];
+$old = ['email' => ''];
+$spamGuard = new SpamGuard();
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+    if (!Csrf::verify(Input::post('csrf_token'))) {
+        return Response::redirect('/newsletter?error=security');
+    }
+
+    $old = [
+        'email' => Input::post('email', ''),
+    ];
+
+    $validator = (new FormValidator())
+        ->required($old['email'], 'email', 'Email is required.')
+        ->email($old['email'], 'email', 'A valid email is required.');
+
+    if ($validator->passes()) {
+        if (!$spamGuard->isSpam(Input::post())) {
+            // ...store $old['email'] somewhere: a file, SQLite once that
+            // groundwork lands (see novaconium/ISSUES.md), a third-party API, etc.
+            // This is the one line that's actually specific to this form.
+        }
+
+        return Response::redirect('/newsletter?sent=1');
+    }
+
+    $errors = $validator->errors();
+}
+
+return [
+    'errors'        => $errors,
+    'old'           => $old,
+    'sent'          => Input::get('sent') !== null,
+    'securityError' => Input::get('error') === 'security',
+    'renderedAt'    => $spamGuard->renderedAt(),
+    'csrfField'     => Csrf::fieldName(),
+    'csrfToken'     => Csrf::token(),
+];{% endverbatim %}
+ +

Four things worth noticing, all covered in full in {{ icons.book() }}Sidecars' "Form security" section:

+ +
    +
  • FormValidator accumulates named field errors — required(), email(), and maxLength()/minLength() are all available; chain as many as the form needs, then check $validator->passes() once.
  • +
  • SpamGuard::isSpam(Input::post()) is checked after validation passes, and the form redirects to the same success URL either way — a bot gets an identical response to a human, it just never reaches the "actually do something" line.
  • +
  • $spamGuard->renderedAt() goes into the sidecar's returned context on every request (GET and POST) — the template needs it either way, since a failed validation re-renders the form with a fresh timestamp for the next attempt.
  • +
  • Csrf::verify() is checked before anything else, and unlike a failed spam check, a failed CSRF check shows a real, visible error — a CSRF failure is usually a legitimate stale-tab case for a real visitor, not something worth hiding.
  • +
+ +

3. The template

+ +

The honeypot field and the hidden timestamp are the two pieces every form needs regardless of its actual fields — copy them as-is:

+ +
{% verbatim %}{% extends layout %}
+
+{% block title %}Newsletter{% endblock %}
+{% block description %}Sign up for occasional updates.{% endblock %}
+
+{% block content %}
+    <article>
+        <h1>Newsletter</h1>
+
+        {% if sent %}
+            <p><strong>Thanks — you're signed up.</strong></p>
+        {% endif %}
+
+        {% if securityError %}
+            <p><strong>Your session expired before submitting — please try again.</strong></p>
+        {% endif %}
+
+        <form method="post" action="/newsletter">
+            <input type="hidden" name="{{ csrfField }}" value="{{ csrfToken }}">
+            <div class="hp-field" aria-hidden="true">
+                <label for="website">Leave this field blank</label>
+                <input type="text" id="website" name="website" tabindex="-1" autocomplete="off">
+            </div>
+            <input type="hidden" name="rendered_at" value="{{ renderedAt }}">
+            <p>
+                <label for="email">Email</label><br>
+                <input type="text" id="email" name="email" value="{{ old.email }}">
+                {% if errors.email %}<br><small>{{ errors.email }}</small>{% endif %}
+            </p>
+            <button type="submit">Sign up</button>
+        </form>
+    </article>
+{% endblock %}{% endverbatim %}
+ +

The .hp-field class (defined once in novaconium/sass/main.sass) positions the honeypot off-screen rather than hiding it with display: none, since some spam bots specifically skip fields hidden that way — see {{ icons.book() }}Sidecars for why. Nothing about that field or the timestamp needs to change between forms; only the real fields (email here, name/email/message on the contact form) differ.

+ +

Why it's never cached

+ +

Because this page has a sidecar, it's excluded from the static-cache path entirely (see {{ icons.book() }}Static caching) — every request re-runs the sidecar, which is exactly what a form needs: a fresh rendered_at timestamp, current validation errors, and an up-to-date sent flag from the query string. A form is the canonical example of a page that shouldn't be sidecar-less, even though nothing stops you from technically leaving the sidecar off — without one, there's nothing to receive $_POST at all.

+ +

Going further

+ +
    +
  • Need a phone number, a postal/zip code, or a "confirm email" field? Lib\Validate has isPhone(), isPostalCode()/isZipCode(), and isMatch() — see {{ icons.book() }}Libraries.
  • +
  • Want the submission to actually go somewhere? Swap the comment in step 2 for a call to Lib\Mailer::send() (see the contact form), a new Lib\ class of your own, or — once SQLite groundwork lands (see novaconium/ISSUES.md, not yet built) — a database write.
  • +
  • Multiple forms on one site can each use their own honeypot/timestamp field names by passing constructor arguments to SpamGuard, e.g. new SpamGuard('url', 'ts', 3), so they don't interfere with each other.
  • +
+{% endblock %} diff --git a/novaconium/pages/admin/docs/getting-started/index.twig b/novaconium/pages/admin/docs/getting-started/index.twig new file mode 100644 index 0000000..09cb3e6 --- /dev/null +++ b/novaconium/pages/admin/docs/getting-started/index.twig @@ -0,0 +1,38 @@ +{% extends 'admin/docs/_layout/layout.twig' %} + +{% block title %}Getting started{% endblock %} + +{% block description %}Requirements, running locally, and deploying on Apache.{% endblock %} + +{% block robots %}noindex, nofollow{% endblock %} + +{% block docs_content %} +

Getting started

+ +

Requirements: PHP 8.1+ and, for production, Apache with mod_rewrite and AllowOverride All.

+ +

Run it locally (no Apache needed)

+ +
php -S 127.0.0.1:8000 -t public public/router.php
+ +

public/router.php is a dev-only script that mimics the .htaccess rules (canonical redirects + static cache lookup) so you can develop without Apache. It is never used in production — Apache reads public/.htaccess directly.

+ +

Visit:

+
    +
  • http://127.0.0.1:8000/ — static home page
  • +
+ +

Deploy on Apache

+ +

Point the vhost's document root at public/, make sure mod_rewrite is enabled and AllowOverride All is set for that directory so public/.htaccess takes effect, and it just works — no build step required.

+ +

Adding a new page

+ +

Create a directory under App/pages/ with an index.twig — the directory path is the URL (see Routing). SEO has a ready-to-paste starter template with every overridable block (title, description, Open Graph, Twitter Card) plus a content stub — copy it in and fill in the blanks.

+ +

Or skip the copy-paste entirely:

+ +
php novaconium/bin/create-static-page.php blog/my-new-post
+ +

Scaffolds App/pages/blog/my-new-post/index.twig from that same starter template, with the title pre-filled from the last path segment ("my-new-post" → "My New Post"). The path can be given with or without a trailing .twig or /index.twig — refuses to run if the page already exists, or if any segment is reserved (starts with _, or is literally 404 — see Routing).

+{% endblock %} diff --git a/novaconium/pages/admin/docs/index.twig b/novaconium/pages/admin/docs/index.twig new file mode 100644 index 0000000..502c6cd --- /dev/null +++ b/novaconium/pages/admin/docs/index.twig @@ -0,0 +1,32 @@ +{% extends 'admin/docs/_layout/layout.twig' %} + +{% import '_layout/icons.twig' as icons %} + +{% block title %}Docs{% endblock %} + +{% block description %}Framework documentation: routing, sidecars, forms, libraries, layouts, caching, styling.{% endblock %} + +{% block robots %}noindex, nofollow{% endblock %} + +{% block docs_content %} +

{{ icons.book() }}Project documentation

+

Framework docs, rendered as plain Twig pages — no internet connection needed.

+ +{% endblock %} diff --git a/novaconium/pages/admin/docs/layouts/index.twig b/novaconium/pages/admin/docs/layouts/index.twig new file mode 100644 index 0000000..9030a7b --- /dev/null +++ b/novaconium/pages/admin/docs/layouts/index.twig @@ -0,0 +1,28 @@ +{% extends 'admin/docs/_layout/layout.twig' %} + +{% block title %}Layouts{% endblock %} + +{% block description %}How pages and layouts are overridable, just like Lib\.{% endblock %} + +{% block robots %}noindex, nofollow{% endblock %} + +{% block docs_content %} +

Pages and layouts are overridable, just like Lib\

+ +

Routing doesn't resolve against a single pages/ tree — it resolves against an ordered list of roots: App/pages/ first, then novaconium/pages/ as the framework's fallback default. The default _layout/layout.twig and 404/index.twig ship in novaconium/pages/; a project doesn't need to duplicate them to have a working site, but can override either (or add any page) just by placing a file at the same relative path in App/pages/. Same mechanism as Lib\ overrides in App/lib/, applied to pages.

+ +

Shared layout markup lives in _layout/layout.twig directories. The renderer walks upward from the matched page looking for the nearest one, checking App/pages/ before novaconium/pages/ at every level — so you can override the layout for a whole subtree just by dropping a new _layout/ next to it:

+ +
novaconium/pages/_layout/layout.twig     <- framework default, used unless overridden
+App/pages/_layout/layout.twig            <- overrides the site-wide default (optional)
+App/pages/blog/_layout/layout.twig       <- overrides it for everything under /blog
+ +

A nested layout can extend the parent one (paths are resolved against the page roots, not relative to the current file):

+ +
{% verbatim %}{% extends 'admin/docs/_layout/layout.twig' %}{% endverbatim %}
+ +

Every index.twig extends whichever layout was resolved for it, via the layout variable that's always injected into the context:

+ +
{% verbatim %}{% extends layout %}
+{% block content %}...{% endblock %}{% endverbatim %}
+{% endblock %} diff --git a/novaconium/pages/admin/docs/libraries/index.twig b/novaconium/pages/admin/docs/libraries/index.twig new file mode 100644 index 0000000..501b94d --- /dev/null +++ b/novaconium/pages/admin/docs/libraries/index.twig @@ -0,0 +1,30 @@ +{% extends 'admin/docs/_layout/layout.twig' %} + +{% block title %}Libraries{% endblock %} + +{% block description %}Plain PHP classes under the Lib\ namespace.{% endblock %} + +{% block robots %}noindex, nofollow{% endblock %} + +{% block docs_content %} +

Libraries

+ +

Plain PHP classes live in App/lib/ (or novaconium/lib/ for defaults) under the Lib\ namespace and are autoloaded automatically — call them from any sidecar:

+ +
use Lib\Mailer;
+
+(new Mailer())->send($old['name'], $old['email'], $old['message']);
+ +

Framework-default classes

+ +

novaconium/lib/ ships a few ready-to-use classes any sidecar can call, same as a project's own App/lib/ classes — override any of them by dropping a same-named file in App/lib/:

+ +
    +
  • Lib\Mailer — the contact form's mail stand-in (logs to a file instead of an external mail dependency; see its own source for the note on swapping in a real mail call).
  • +
  • Lib\SpamGuard — self-hosted honeypot + submission-timing spam detection, reusable on any form. See Sidecars' "Spam prevention" section for the full write-up.
  • +
  • Lib\FormValidator — a small accumulating required-field/email/length validator, so a form sidecar doesn't hand-roll the same checks and $errors array every time. Also covered in Sidecars' "Spam prevention" section.
  • +
  • Lib\Validate — the lower-level validation primitives FormValidator calls into (isEmail(), minLength()/maxLength(), isMatch(), isPhone(), isPostalCode()/isZipCode()) — call these directly from a sidecar when you just need a validated/normalized value back rather than an accumulated field-error. See Sidecars' "Spam prevention" section.
  • +
  • Lib\Input — a cleaning accessor for $_POST/$_GET (Input::post()/Input::get()), used by every sidecar instead of the superglobals directly. Defense-in-depth against HTML/script injection, not a defense against SQL injection — see Sidecars' "Form security" section for the full caveat.
  • +
  • Lib\Csrf — standalone session-token CSRF protection (Csrf::token()/::verify()), called directly from a sidecar rather than through FormValidator. See Sidecars' "Form security" section.
  • +
+{% endblock %} diff --git a/novaconium/pages/admin/docs/matomo/index.twig b/novaconium/pages/admin/docs/matomo/index.twig new file mode 100644 index 0000000..f6806a6 --- /dev/null +++ b/novaconium/pages/admin/docs/matomo/index.twig @@ -0,0 +1,46 @@ +{% extends 'admin/docs/_layout/layout.twig' %} + +{% block title %}Matomo{% endblock %} + +{% block description %}Built-in Matomo analytics tracking, including 404 page tracking.{% endblock %} + +{% block robots %}noindex, nofollow{% endblock %} + +{% block docs_content %} +

Matomo analytics

+ +

The root layout (novaconium/pages/_layout/layout.twig) includes _layout/matomo.twig, which emits the standard Matomo tracking snippet on every page — off by default, enabled by supplying two App/config.php keys.

+ +

Enabling it

+ +
<?php
+// App/config.php
+return [
+    'matomo_url'     => 'https://matomo.example.com/',
+    'matomo_site_id' => '1',
+];
+ +

Both matomo_url and matomo_site_id default to an empty string in novaconium/config.php. The layout only renders the tracking <script> when both are set — leaving either one out (e.g. in local development) disables tracking entirely, with zero script emitted. A missing trailing slash on matomo_url is normalized automatically in novaconium/bootstrap.php.

+ +

404 tracking

+ +

Matomo doesn't know a page is a 404 unless told — its documented convention is to set the document title to 404/URL = <requested path>/From = <referrer> immediately before trackPageView, so 404 hits are filterable under Behaviour → Page URLs by searching for 404. The layout does this automatically: novaconium/src/Renderer.php::renderNotFound() passes is_404 => true into the 404 template's context (a Twig global elsewhere, defaulting to false), and the layout's tracking script checks that flag to decide whether to push setDocumentTitle before trackPageView.

+ +

What's included

+ +
    +
  • trackPageView on every request.
  • +
  • enableLinkTracking for outbound-link and download tracking.
  • +
  • 404-specific document title tagging as described above.
  • +
+ +

The snippet is the same one Matomo's own admin UI generates ("JS Tracking Code"), so anything from Matomo's docs that builds on _paq.push([...]) calls (custom events, goal tracking, ecommerce, etc.) can be added directly to it.

+ +

Overriding the snippet

+ +

The snippet lives in its own file, novaconium/pages/_layout/matomo.twig, specifically so it's overridable — same App-over-novaconium mechanism used for every other page and layout, not a special case. Drop a same-named file at App/pages/_layout/matomo.twig to replace it entirely without touching novaconium/: point at a self-hosted tracker proxy, add extra _paq.push calls, gate it behind a consent check, or swap in a completely different analytics snippet. matomo_url, matomo_site_id, and is_404 are all available in the override's context, same as in the original.

+ +

Privacy note

+ +

This only wires up the tracking snippet — it does not add a cookie/consent banner or handle Do Not Track. If your jurisdiction requires consent before loading analytics, gate the matomo_url/matomo_site_id config or override _layout/matomo.twig behind whatever consent mechanism the project uses.

+{% endblock %} diff --git a/novaconium/pages/admin/docs/project-layout/index.twig b/novaconium/pages/admin/docs/project-layout/index.twig new file mode 100644 index 0000000..d8d6f8f --- /dev/null +++ b/novaconium/pages/admin/docs/project-layout/index.twig @@ -0,0 +1,56 @@ +{% extends 'admin/docs/_layout/layout.twig' %} + +{% block title %}Project layout{% endblock %} + +{% block description %}A map of the whole project tree.{% endblock %} + +{% block robots %}noindex, nofollow{% endblock %} + +{% block docs_content %} +

Project layout

+ +
public/             Apache document root
+  index.php           thin front controller — just requires novaconium/bootstrap.php
+  .htaccess            cache short-circuit, canonical redirects, rewrite to index.php
+  router.php            dev-only helper for `php -S` (not used in production)
+  cache/                 generated static HTML (safe to delete anytime)
+  css/                    compiled CSS output
+App/                 your project — the only directory you're expected to edit
+  pages/                your routes — directory tree = URL tree, checked before novaconium/pages/ so you can add or override pages/layouts
+  lib/                  your PHP classes (Lib\), checked before novaconium/lib/ so you can override defaults
+  sass/                 your Sass overrides — _colors.sass, checked before novaconium/sass/defaults/
+  config.php            your config overrides — ships as an empty, commented placeholder; shallow-merged over novaconium/config.php
+novaconium/          the framework itself — boilerplate, not meant to be edited per-project
+  pages/                default pages: _layout/layout.twig (root layout) and 404/index.twig (used when App/pages/ doesn't override them)
+  sass/                 Sass source: main.sass (structure) + defaults/_colors.sass (default palette, overridable from App/sass/)
+  lib/                  default Lib\ classes (used when App/lib/ doesn't override them)
+  src/                   Router, Route, Renderer, Response, Cache, Overlay (the App/-over-novaconium/ lookup used for both pages and lib)
+  vendor/twig/            vendored Twig source (no Composer)
+  bin/
+    clear-cache.php         standalone CLI entry point — `php novaconium/bin/clear-cache.php`
+    create-static-page.php    scaffolds a new page from the SEO starter template — `php novaconium/bin/create-static-page.php `
+  autoload.php           manual PSR-4 autoloader
+  config.php              paths and settings
+  bootstrap.php            wires everything together for each request
+ +

How a request flows through these files

+ +

There's no framework "kernel" class — just a plain chain of requires, each handing off to the next, deliberately kept flat enough to read top-to-bottom in one sitting:

+ +
    +
  1. public/.htaccess runs first, before PHP does anything. If public/cache/<path>/index.html exists for the requested URL, Apache serves that file directly and nothing below this line ever executes — see Static caching. Otherwise it strips a trailing slash (301 redirect) and rewrites everything else to public/index.php.
  2. +
  3. public/index.php is intentionally one line: require __DIR__ . '/../novaconium/bootstrap.php';. (Running locally via php -S instead of Apache? public/router.php mimics the same three .htaccess rules in PHP, then requires index.php the same way — see Getting started.)
  4. +
  5. novaconium/bootstrap.php is where the real wiring happens, top-to-bottom: +
      +
    1. Requires novaconium/autoload.php, registering the Twig\/App\/Lib\ class autoloader (see Libraries) before anything below tries to instantiate a class.
    2. +
    3. Requires novaconium/config.php, then shallow-merges App/config.php over it if that file exists — see Configuration.
    4. +
    5. Special-cases /admin/logout directly against the request path — see Admin authentication — since it isn't a real page for the router to find.
    6. +
    7. Constructs a Router (novaconium/src/Router.php) and calls resolve() to turn the URL into a Route (novaconium/src/Route.php) — see Routing.
    8. +
    9. If the resolved route is under admin/admin/*, calls AdminAuth::requireLogin() (novaconium/src/AdminAuth.php), reading that same Route.
    10. +
    11. Constructs a Cache (novaconium/src/Cache.php) and a Renderer (novaconium/src/Renderer.php), then calls renderNotFound() or render($route, ...) depending on $route->found — see Sidecars for what happens inside Renderer itself (running the matched directory's index.php, if any; resolving the nearest layout; rendering index.twig; writing the static cache for sidecar-less pages).
    12. +
    +
  6. +
+ +

Every step after step 1 is plain PHP you can read start to finish in novaconium/bootstrap.php itself — the comments there walk through the same five sub-steps in more detail than this page does.

+{% endblock %} diff --git a/novaconium/pages/admin/docs/routing/index.twig b/novaconium/pages/admin/docs/routing/index.twig new file mode 100644 index 0000000..78b759f --- /dev/null +++ b/novaconium/pages/admin/docs/routing/index.twig @@ -0,0 +1,73 @@ +{% extends 'admin/docs/_layout/layout.twig' %} + +{% block title %}Routing{% endblock %} + +{% block description %}How a URL maps to a directory under App/pages/.{% endblock %} + +{% block robots %}noindex, nofollow{% endblock %} + +{% block docs_content %} +

How routing works

+ +

A URL maps directly to a directory under App/pages/:

+ +
App/pages/
+  index.twig              ->  /
+  about/
+    index.twig             ->  /about
+  products/
+    [id]/
+      index.twig             ->  /products/<anything>   ($params['id'] = <anything>)
+      index.php               ->  optional sidecar for that page
+ +
    +
  • Every route is App/pages/<segments>/index.twig (and/or index.php — see Sidecars). There are no flat about.twig files and no route table to maintain.
  • +
  • A directory named [param] matches any single URL segment and captures it into $params['param'] — that's how you get clean URLs like /products/socks with no query string. (This project's own App/pages/blog/ doesn't currently use this — every post there is a plain directory named after its slug, e.g. App/pages/blog/hello-world/ — but the mechanism is still fully supported.)
  • +
  • Canonical URLs never have a trailing slash. /about/ 301-redirects to /about.
  • +
  • Directories starting with _ (like _layout/) and a directory literally named 404 are reserved and can never be requested directly.
  • +
+ +

For developers: using Router.php directly

+ +

novaconium/src/Router.php is the class that does the walk described above. It's deliberately a pure lookup — given a URL, it answers "does a page exist here, and if so which directory and what params?" — and knows nothing about Twig, sidecars, caching, or output. It's also what makes Router easy to use in isolation — in a test, a script, or anywhere you just want "what would this URL resolve to?" without booting the whole render pipeline.

+ +

How Route.php fits in

+ +

novaconium/src/Route.php is the value object Router::resolve() returns — three readonly properties (found, dir, params) and a Route::notFound() factory, no behavior at all. It's the thing that actually flows through the rest of the request, decoupling Router from everything downstream: Router produces a Route and is done, and every later step only ever reads it. See novaconium/bootstrap.php, which runs top-to-bottom as a plain script rather than through a framework "kernel" class:

+ +
    +
  1. Config loads (framework defaults + optional App/config.php override).
  2. +
  3. /admin/logout is special-cased directly against the raw request path, before routing even runs — it isn't a real page, so there'd be no Route for it anyway.
  4. +
  5. Router::resolve() runs and returns a Route — this is the only place a Route gets created.
  6. +
  7. If $route->dir is under admin/admin/*, AdminAuth::requireLogin() gates it — reading $route->dir directly off the value object Router handed back.
  8. +
  9. Renderer takes over, also just reading the same Route: renderNotFound() if $route->found is false, otherwise render($route, ...) — using $route->dir to find the sidecar/layout/template and $route->params as template context.
  10. +
+ +

The key point: Route is passed around, not Router itself — bootstrap.php only calls Router::resolve() once, and the plain data object it gets back is what AdminAuth and Renderer both independently inspect afterward. Neither of them needs a reference to Router, Overlay, or the page-root filesystem lookup that produced the Route — that's why adding a new admin page, or a new reserved segment, or a new render behavior never means touching Route.php itself; it stays a dumb, stable carrier.

+ +

Constructing it

+ +
use App\Router;
+
+$router = new Router($config['pages_dirs']);
+ +

The constructor takes the same ordered list of page roots as everything else in this framework — App/pages/ first, novaconium/pages/ as fallback (see novaconium/config.php's pages_dirs). Order matters: it's what lets a project override a page just by placing one at the same relative path in App/pages/.

+ +

resolve() and the Route it returns

+ +
$route = $router->resolve('/blog/hello-world');
+
+$route->found;   // bool — true if a real page/sidecar exists at this path
+$route->dir;     // ?string — the matched directory, relative to the page roots
+                  //           (e.g. "blog/hello-world"), or null if not found
+$route->params;  // array<string,string> — captured [param] segments, e.g.
+                  //           ['id' => 'socks'] for a /products/[id]/ route
+ +

resolve() takes a full request URI (query string and all — it strips that internally with strtok($requestUri, '?')) and returns a Route value object (novaconium/src/Route.php): three readonly properties, no behavior. A not-found result is just Route::notFound()dir and params are null/empty, found is false. There's no exception thrown for a 404; check $route->found the same way bootstrap.php does.

+ +

What actually makes something "found"

+ +

Walking the URL segment by segment, Router resolves each one against the page roots via Overlay (see novaconium/src/Overlay.php): an exact-name subdirectory wins if one exists in either root, otherwise a [param]-named subdirectory captures the segment. A segment starting with _ or literally named 404 short-circuits straight to not-found, regardless of what's on disk — those are always reserved. At the end of the walk, the resolved directory still has to contain an index.twig or an index.php (a JSON-only API endpoint, say, can skip the template entirely) — no template and no sidecar means not-found even if every segment matched a real directory along the way.

+ +

Since Router has no dependencies beyond Overlay and does no I/O beyond filesystem existence checks, it's straightforward to exercise directly — construct it with a real (or temporary/fixture) pagesDirs array and assert on the Route it returns, without needing to spin up the full HTTP request cycle.

+{% endblock %} diff --git a/novaconium/pages/admin/docs/seo/index.twig b/novaconium/pages/admin/docs/seo/index.twig new file mode 100644 index 0000000..3fc2c4f --- /dev/null +++ b/novaconium/pages/admin/docs/seo/index.twig @@ -0,0 +1,94 @@ +{% extends 'admin/docs/_layout/layout.twig' %} + +{% block title %}SEO{% endblock %} + +{% block description %}The SEO meta tags every page gets for free, and how to override them per-page.{% endblock %} + +{% block robots %}noindex, nofollow{% endblock %} + +{% block docs_content %} +

SEO boilerplate

+ +

novaconium/pages/_layout/layout.twig (the root layout every page extends, directly or via a nested layout) renders a full set of SEO meta tags in <head>: viewport, description, robots, canonical link, Open Graph, and Twitter Card. Each piece is a named Twig block with a sensible default, so any page can override just the piece it needs without touching the rest of <head>.

+ +

Blocks you can override

+ + + + + + + + + + + + + + + + + + +
BlockDefaultRenders as
titlesite_name config value<title>, and reused by og:title / twitter:title
descriptiongeneric site description<meta name="description">, and reused by og:description / twitter:description
robotsindex, follow<meta name="robots">
canonical{{ '{{ request_path }}' }}<link rel="canonical">, and reused by og:url
og_typewebsite<meta property="og:type">
og_title{{ '{{ block(\'title\') }}' }}<meta property="og:title">
og_description{{ '{{ block(\'description\') }}' }}<meta property="og:description">
og_url{{ '{{ block(\'canonical\') }}' }}<meta property="og:url">
twitter_cardsummary<meta name="twitter:card">
twitter_title{{ '{{ block(\'title\') }}' }}<meta name="twitter:title">
twitter_description{{ '{{ block(\'description\') }}' }}<meta name="twitter:description">
+ +

Blocks that piggyback on another block (e.g. og_title defaulting to {{ '{{ block(\'title\') }}' }}) use Twig's block() function, not inheritance — so setting title alone is enough to update og:title and twitter:title too, unless the page also overrides those blocks explicitly.

+ +

Overriding on a page

+ +

Any index.twig can override any subset of these blocks, same as title or content:

+ +
{% verbatim %}{% extends layout %}
+
+{% block title %}Pricing{% endblock %}
+{% block description %}Plans and pricing for the whole team.{% endblock %}
+{% block og_type %}product{% endblock %}
+
+{% block content %}
+    ...
+{% endblock %}{% endverbatim %}
+ +

See any post under App/pages/blog/ (e.g. App/pages/blog/twig-syntax-guide/index.twig) for a working example that sets og_type to article with a hand-written description. If you ever need to derive a description from a longer body string, don't reach for Twig's |slice filter — applied to a string, it calls mb_substr() unconditionally with no fallback, which hard-requires the mbstring extension. Truncate in PHP instead, guarded with function_exists('mb_substr'); see the footnote on the Twig Syntax Guide for the story of why this project cares.

+ +

Copy-paste starter: every block, explicitly

+ +

Every App/pages/*/index.twig page in this project already includes the full block below as a reference — copy it into a new page and fill in the blanks. Nothing here is required (the layout's defaults are fine on their own), but having every knob visible up front makes it obvious what's available. Don't want to copy-paste by hand? php novaconium/bin/create-static-page.php <path> scaffolds this exact template for you — see Getting started.

+ +
{% verbatim %}{% extends layout %}
+
+{% block title %}Page title{% endblock %}
+{% block description %}One or two sentences describing this page.{% endblock %}
+
+{% block robots %}index, follow{% endblock %}
+{% block canonical %}{{ request_path|default('/') }}{% endblock %}
+
+{% block og_type %}website{% endblock %}
+{% block og_title %}{{ block('title') }}{% endblock %}
+{% block og_description %}{{ block('description') }}{% endblock %}
+{% block og_url %}{{ block('canonical') }}{% endblock %}
+
+{% block twitter_card %}summary{% endblock %}
+{% block twitter_title %}{{ block('title') }}{% endblock %}
+{% block twitter_description %}{{ block('description') }}{% endblock %}
+
+{% block content %}
+    <article>
+        <h1>Page title</h1>
+        <p>...</p>
+    </article>
+{% endblock %}{% endverbatim %}
+ +

To keep a page out of search results, change only the robots line to noindex, nofollow and delete the rest — everything else can be left to the layout's defaults, same as the /admin pages do.

+ +

Canonical URLs and request_path

+ +

The canonical link (and og:url) default to a request_path variable that novaconium/src/Renderer.php injects into every template's context — the request URI's path component, computed via parse_url($requestUri, PHP_URL_PATH). You don't need to set this yourself; it's already correct for every route, including the 404 page. If you want an absolute canonical URL instead of a path (e.g. https://example.com/about rather than /about), override the canonical block per-page or add a site-wide base URL to novaconium/config.php and reference it in the layout.

+ +

Keeping admin/internal pages out of search results

+ +

Pages that shouldn't be indexed — /admin, /admin/clear-cache, every /admin/docs/* page, and the 404 page — override robots to noindex, nofollow. Follow the same pattern for any project-specific admin or internal tooling pages you add under App/pages/.

+ +

Favicon

+ +

The layout links <link rel="icon" href="/favicon.ico"> unconditionally — drop a favicon.ico into public/ to have it picked up; there's no fallback or generation step.

+{% endblock %} diff --git a/novaconium/pages/admin/docs/sidecars/index.twig b/novaconium/pages/admin/docs/sidecars/index.twig new file mode 100644 index 0000000..632a2fb --- /dev/null +++ b/novaconium/pages/admin/docs/sidecars/index.twig @@ -0,0 +1,242 @@ +{% extends 'admin/docs/_layout/layout.twig' %} + +{% block title %}Sidecars{% endblock %} + +{% block description %}Where your PHP logic goes — the optional index.php sidecar.{% endblock %} + +{% block robots %}noindex, nofollow{% endblock %} + +{% block docs_content %} +

Sidecars — where your PHP logic goes

+ +

Drop an index.php next to any index.twig and it becomes that page's data provider. It runs before the template and can return one of two things:

+ +

An array — becomes the Twig context

+ +
<?php
+// App/pages/contact/index.php
+use Lib\Input;
+use Lib\Mailer;
+
+$errors = [];
+$old = ['name' => '', 'email' => '', 'message' => ''];
+
+if ($_SERVER['REQUEST_METHOD'] === 'POST') {
+    // ...validate Input::post() into $old/$errors...
+
+    if (!$errors) {
+        (new Mailer())->send($old['name'], $old['email'], $old['message']);
+        return \App\Response::redirect('/contact?sent=1');
+    }
+}
+
+return ['errors' => $errors, 'old' => $old];
+ +

$params (the captured route segments, e.g. an [id] directory's id) is already in scope — no need to touch $_GET.

+ +

A Response object — short-circuits Twig entirely

+ +
use App\Response;
+
+Response::redirect('/somewhere');
+Response::json(['ok' => true]);
+Response::xml('<root/>');
+Response::html('<h1>raw</h1>');
+ +

A sidecar-only page (no index.twig at all) is fine too — e.g. an App/pages/api/posts/index.php returning just Response::json([...]) for a JSON-only endpoint. Twig never runs for that route at all; the sidecar's return value is the entire response.

+ +

Form handling — a sidecar can branch on $_SERVER['REQUEST_METHOD'], validate $_POST, call a library, and only return Response::redirect() once it succeeds (the classic POST/redirect/GET pattern, so a page refresh doesn't resubmit the form). See App/pages/contact/index.php + App/pages/contact/index.twig for the full example — it validates name/email/message, calls Lib\Mailer::send(), and redirects to /contact?sent=1 to show a success message.

+ +

Form security

+ +

Every form on this site combines three independent layers, all reusable Lib\ classes: input cleaning, CSRF protection, and spam prevention. None of them depend on each other — a form can use any subset.

+ +

Input cleaning

+ +

Lib\Input (novaconium/lib/Input.php) is a drop-in replacement for reading $_POST/$_GET directly — every sidecar on this site uses it instead of touching the superglobals:

+ +
use Lib\Input;
+
+$name  = Input::post('name', '');      // trimmed, tags stripped, null bytes removed
+$sent  = Input::get('sent') !== null;  // was ?sent= present at all?
+$all   = Input::post();                // the whole cleaned $_POST array
+ +

Calling post()/get() with no key returns the entire cleaned array (handy for passing straight to SpamGuard::isSpam(), as below); calling it with a key returns that key's cleaned value, or the given default if it's absent. Nested arrays (e.g. a checkbox group posted as tags[]) are cleaned recursively. The result is memoized per request, so calling Input::post() repeatedly across a sidecar doesn't re-clean the superglobal each time.

+ +

This is not SQL-injection protection. The cleaning Input does (trim + strip tags, via Lib\Validate::clean(), plus null-byte stripping) is defense-in-depth against HTML/script injection in output contexts — Twig already autoescapes {{ }} output by default (see novaconium/src/Renderer.php), so this is a second layer, not the only one. No string transform makes arbitrary input safe to concatenate into a SQL query; the real defense is parameterized queries (PDO prepared statements). There's no database layer in this framework yet (SQLite groundwork is Backlog — see novaconium/ISSUES.md); when one lands, use prepared statements exclusively. Input deliberately has no sqlSafe()-style method, since a method implying "cleaned = safe to interpolate into SQL" would be actively dangerous.

+ +

One documented exception: a field that needs an exact, unmodified value — a password about to be hashed, say — should read $_POST directly instead of going through Input::post(). Cleaning would silently strip characters like </> before hashing, producing a hash that doesn't match what's actually typed later. See novaconium/pages/admin/password-hash/index.php for the one place this framework does that on purpose.

+ +

CSRF protection

+ +

Lib\Csrf (novaconium/lib/Csrf.php) is a standalone session-token CSRF guard — standalone meaning it isn't wired into FormValidator's chain, so a sidecar calls it directly, typically as the very first check on a POST:

+ +
use Lib\Csrf;
+use Lib\Input;
+
+if (!Csrf::verify(Input::post('csrf_token'))) {
+    return \App\Response::redirect('/contact?error=security');
+}
+ +

with a matching hidden field alongside the honeypot/timestamp fields:

+ +
<input type="hidden" name="{{ csrfField }}" value="{{ csrfToken }}">
+ +

and two extra keys in the sidecar's returned context:

+ +
'csrfField' => Csrf::fieldName(),
+'csrfToken' => Csrf::token(),
+ +

Csrf::token() is idempotent per session — it doesn't rotate on every call, so a form that re-renders after a validation error still verifies correctly on the next submit. It's the first thing in the framework that starts a native PHP session, and only lazily: a page that never calls Csrf never gets a session cookie, so the rest of the site stays session-free. The session cookie itself is hardened (httponly, SameSite=Lax, secure when the request is HTTPS) inside Csrf's private ensureSession().

+ +

A failed CSRF check shows a real, visible error — "Your session expired before submitting — please try again." — unlike a failed spam check, which redirects to the same success URL either way. That's deliberate: a CSRF failure is usually a legitimate stale-tab case for a real visitor, not something worth hiding, whereas hiding a spam block from a bot is the whole point of that check.

+ +

Spam prevention

+ +

The contact form fends off basic spam without an external CAPTCHA service (no CDN script, no site key/secret key, no outbound API call on every submission — consistent with this project's self-hosted-everything approach), using Lib\SpamGuard (novaconium/lib/SpamGuard.php) — a framework-default Lib\ class, reusable on any form a project adds, the same way Lib\Mailer is:

+ +
use Lib\Input;
+use Lib\SpamGuard;
+
+$spamGuard = new SpamGuard();       // honeypot field 'website', timestamp field 'rendered_at', 2s minimum
+
+if (!$spamGuard->isSpam(Input::post())) {
+    // ...actually send the message...
+}
+
+// In the sidecar's returned context, for the hidden timestamp field:
+'renderedAt' => $spamGuard->renderedAt(),
+ +

Two checks run inside isSpam(), both purely server-side:

+ +
    +
  • Honeypot field. The paired Twig template renders a website input inside a .hp-field wrapper, positioned off-screen with CSS (position: absolute; left: -9999px — deliberately not display: none or visibility: hidden, since some spam bots specifically skip fields hidden that way) and marked aria-hidden="true" with tabindex="-1" so it's invisible to real visitors and screen readers alike. A bot that auto-fills every input on a form fills this one too; any non-empty value counts as spam.
  • +
  • Timing check. A hidden field (rendered via $spamGuard->renderedAt(), read back as rendered_at) carries the Unix timestamp of when the form was rendered. On submit, anything completed in under the constructor's $minSeconds (2 by default) counts as spam — plausible for a script filling and submitting a form instantly, implausible for a human reading the form and typing a message. This value isn't cryptographically signed, so a determined bot could forge it; it's a deterrent against unsophisticated spam, not a security boundary.
  • +
+ +

isSpam() tripping either check doesn't stop the sidecar from validating and redirecting normally — see App/pages/contact/index.php, which still returns Response::redirect('/contact?sent=1') regardless, and only skips Lib\Mailer::send() when spam is detected. That's deliberate: a bot gets the exact same success response a human would, with nothing revealing which check it tripped, or that a check exists at all. Both the honeypot field name, timestamp field name, and minimum seconds are constructor arguments (new SpamGuard('website', 'rendered_at', 2)), so a second form on the same site can use different field names without the two forms interfering with each other.

+ +

Field validation (required fields, email format, length limits) is its own reusable class, Lib\FormValidator (novaconium/lib/FormValidator.php) — an accumulating validator so a sidecar doesn't hand-roll the same checks and $errors array every time:

+ +
use Lib\FormValidator;
+
+$validator = (new FormValidator())
+    ->required($old['name'], 'name', 'Name is required.')
+    ->email($old['email'], 'email', 'A valid email is required.')
+    ->required($old['message'], 'message', 'Message is required.')
+    ->maxLength($old['message'], 'message', 2000, 'Message is too long.');
+
+if ($validator->passes()) {
+    // ...
+}
+
+$errors = $validator->errors();
+ +

FormValidator doesn't implement validation logic itself — each check delegates to Lib\Validate (novaconium/lib/Validate.php), a set of stateless, static validation primitives modeled after the project author's own reusable validation class: clean(), isEmail(), minLength()/maxLength(), isMatch() (e.g. a "confirm email" field), isPhone() (7- or 10-digit, with optional extension), and isPostalCode()/isZipCode(). Call Validate directly from a sidecar when you just need a validated/normalized value back — e.g. Validate::isPhone($old['phone'], withExtension: true) — rather than an accumulated field-error:

+ +
use Lib\Validate;
+
+$phone = Validate::isPhone($old['phone']);   // '5551234567' or false
+ +

All three classes live under novaconium/lib/ as framework defaults — like any other Lib\ class, a project can override any of them by dropping a same-named file in App/lib/ (see Libraries).

+ +

Copy-paste starter: a sidecar, three ways

+ +

Drop this in as App/pages/example/index.php (next to an App/pages/example/index.twig using the SEO starter template) and delete whichever example you don't need:

+ +
{% verbatim %}<?php
+// App/pages/example/index.php
+
+// 1. Say hello — becomes Twig context, so {{ message }} works in index.twig.
+return [
+    'message' => 'Hello, World!',
+];
+
+// 2. Show the visitor's IP — same idea, just another key in the array.
+// return [
+//     'ip' => $_SERVER['REMOTE_ADDR'] ?? 'unknown',
+// ];
+
+// 3. Run phpinfo() — a Response short-circuits Twig entirely, which
+// phpinfo() needs since it echoes its own complete HTML page. Capture
+// that output with ob_start()/ob_get_clean() and hand it to
+// Response::html() rather than letting phpinfo() echo directly (which
+// would print before whatever index.twig renders, out of order).
+// use App\Response;
+//
+// ob_start();
+// phpinfo();
+// return Response::html(ob_get_clean());{% endverbatim %}
+ +

Only one of the three returns in a real sidecar ever runs, obviously — pick one, or branch between them with an if. The IP example works with or without a sidecar-only page (no index.twig); the phpinfo() example needs no index.twig at all, since Response::html() bypasses Twig — see the JSON-only example above for another sidecar-only page.

+ +

Never ship phpinfo() to production — it dumps environment variables, file paths, loaded extensions, and configuration values that are useful to an attacker mapping your server. Delete the page after you're done with it, or at minimum gate it behind admin authentication the same way /admin/* already is, so it's never reachable by the public.

+ +

For developers: using Response.php directly

+ +

novaconium/src/Response.php is the small value object behind Response::redirect()/::json()/::xml()/::html() above. Like Route (see /admin/docs/routing's "How Route.php fits in" section), it's a dumb data carrier with one job: describe a response, without emitting anything itself, so it can be constructed in a sidecar and only actually acted on later, by Renderer.

+ +

How it fits in

+ +

Its constructor is private — you never call new Response(...) directly, only one of the four named factories, each of which fixes the type/headers that go with that kind of response:

+ +
Response::redirect(string $url, int $status = 301): self   // type 'redirect', no extra headers
+Response::json(mixed $data, int $status = 200): self       // type 'json', Content-Type: application/json
+Response::xml(string $xml, int $status = 200): self         // type 'xml',  Content-Type: application/xml
+Response::html(string $html, int $status = 200): self       // type 'html', Content-Type: text/html
+ +

Every factory returns a fully-formed, readonly Responsetype, body, status, headers — and nothing has happened yet. A sidecar just returns that object; it's Renderer::render() (see this page's Renderer.php section above) that checks $result instanceof Response and, if so, calls $result->emit() and stops, skipping Twig entirely. That's the entire contract between a sidecar and the framework: return an array for Twig context, or return a Response to bypass it.

+ +

What emit() actually does

+ +

emit() is the one method with side effects, and it's only ever called from inside Renderer, never from a sidecar itself:

+ +
    +
  1. Sets the HTTP status code via http_response_code($this->status).
  2. +
  3. Sends each header in $this->headers (empty for redirect, a single Content-Type for the other three).
  4. +
  5. For a redirect, sends a Location header (the URL passed to Response::redirect()) and returns — no body.
  6. +
  7. For json, encodes $this->body with json_encode(..., JSON_THROW_ON_ERROR) and echoes it — the JSON_THROW_ON_ERROR flag means an unencodable value (e.g. a resource, or a value containing invalid UTF-8) throws a JsonException rather than silently emitting false as the body.
  8. +
  9. For xml/html, $this->body is already a string (built by the caller), so it's echoed as-is — Response doesn't validate or escape it.
  10. +
+ +

Because every property is readonly and the type/body/status/headers are fixed at construction, a Response is safe to build early in a sidecar and pass around (or return immediately) without worrying about it changing shape before Renderer gets to it — there's no setter to call by mistake.

+ +

For developers: using Renderer.php directly

+ +

novaconium/src/Renderer.php is the class that actually runs a sidecar and turns its return value into a response — everything in this page so far (arrays becoming Twig context, Response objects short-circuiting) is Renderer's doing. Unlike Router (see /admin/docs/routing's "For developers" section), it isn't a pure lookup: render() and renderNotFound() both emit directly — http_response_code(), header(), echo — rather than returning a string, so it's a class you call once per request for its side effects, not one you inspect a return value from.

+ +

How it fits in

+ +

Renderer is the last thing a request touches, in novaconium/bootstrap.php: Router::resolve() produces a Route, AdminAuth optionally gates it, and then Renderer reads that same Route to actually produce output. It never talks back to Router or AdminAuth — by the time it runs, routing and auth are already decided, and its only inputs are the Route and the raw request URI:

+ +
$renderer = new Renderer($config['pages_dirs'], $cache, $adminAuthEnabled, $matomoUrl, $config['matomo_site_id'], $config['site_name']);
+
+if (!$route->found) {
+    $renderer->renderNotFound($requestUri);
+    return;
+}
+
+$renderer->render($route, $requestUri);
+ +

Constructing it

+ +

The first two constructor arguments are the same pagesDirs override-root list every other class here takes, plus a Cache instance (novaconium/src/Cache.php) it writes sidecar-less pages' output to. The remaining four — admin-auth-enabled flag, Matomo URL/site ID, site name — aren't used for routing or rendering logic at all; they're registered as Twig globals ($this->twig->addGlobal(...)) purely so every template can read matomo_url, site_name, etc. without a sidecar having to pass them through manually. A fifth global, is_404, defaults to false here and is overridden per-render — see below.

+ +

What render() actually does, in order

+ +
    +
  1. Looks for index.php at $route->dir via Overlay::findFile() (checking App/pages/ before novaconium/pages/, same as everywhere else) and, if one exists, requires it in a scope where $params and $cache are already defined — see runSidecar().
  2. +
  3. If the sidecar returned a Response, calls $result->emit() and returns immediately — Twig never runs, nothing gets cached.
  4. +
  5. Otherwise treats the return value as Twig context, merging in params, the resolved layout path, and request_path.
  6. +
  7. Renders index.twig at $route->dir with that context, emits 200 + the HTML.
  8. +
  9. Only if there was no sidecar, writes the rendered HTML to the static cache (Cache::write()) — a page with a sidecar is never cached this way, since its output can vary per request.
  10. +
+ +

renderNotFound() is a smaller version of the same idea: no sidecar to run, always emits 404, renders 404/index.twig if one exists in either page root (with is_404 forced to true in that render's context — see /admin/docs/matomo for what that flag is used for), and falls back to a bare 404 Not Found string if even the default 404 template is missing.

+ +

Layout resolution

+ +

Both methods get their layout value from the private relativeLayoutPath(), which walks upward from the matched directory looking for the nearest _layout/layout.twig — checking both page roots at every level before going up one more directory — until it either finds one or runs out of directory to walk (returning null, which only happens if even the root _layout/layout.twig is missing from both roots). This is what lets App/pages/blog/_layout/layout.twig override the site-wide layout for just that subtree, per Layouts.

+ +

Because render()/renderNotFound() write straight to PHP's output buffer and response headers rather than returning anything, testing Renderer in isolation means capturing output (e.g. ob_start()) and inspecting headers, rather than asserting on a return value the way you can with Router::resolve()'s Route.

+{% endblock %} diff --git a/novaconium/pages/admin/docs/styling/index.twig b/novaconium/pages/admin/docs/styling/index.twig new file mode 100644 index 0000000..a1b151b --- /dev/null +++ b/novaconium/pages/admin/docs/styling/index.twig @@ -0,0 +1,60 @@ +{% extends 'admin/docs/_layout/layout.twig' %} + +{% block title %}Styling{% endblock %} + +{% block description %}Sass, indented syntax, compiled to public/css/main.css, with an overridable color palette.{% endblock %} + +{% block robots %}noindex, nofollow{% endblock %} + +{% block docs_content %} +

Styling (Sass, indented syntax)

+ +

Source lives in novaconium/sass/main.sass (indented syntax, not SCSS). Compile it with Dart Sass, passing both Sass directories as load paths:

+ +
sass --load-path=App/sass --load-path=novaconium/sass/defaults novaconium/sass/main.sass public/css/main.css
+ +

There's no PHP-based Sass compiler in this project (PHP options like scssphp only understand SCSS syntax) — this is a manual/CI build step, not something the app does at runtime.

+ +

Don't have Dart Sass installed? Run it via Docker instead — copy-paste this Dockerfile, which installs the same official standalone Dart Sass release used in this environment (1.101.0, via pacman -S dart-sass on Arch), not the npm-wrapped build:

+ +
FROM debian:bookworm-slim
+
+RUN apt-get update \
+    && apt-get install -y --no-install-recommends curl ca-certificates \
+    && curl -fsSLo /tmp/dart-sass.tar.gz \
+        https://github.com/sass/dart-sass/releases/download/1.101.0/dart-sass-1.101.0-linux-x64.tar.gz \
+    && tar -xzf /tmp/dart-sass.tar.gz -C /usr/local/lib \
+    && ln -s /usr/local/lib/dart-sass/sass /usr/local/bin/sass \
+    && rm /tmp/dart-sass.tar.gz \
+    && apt-get purge -y curl \
+    && apt-get autoremove -y \
+    && rm -rf /var/lib/apt/lists/*
+
+WORKDIR /usr/src/app
+
+ENTRYPOINT ["sass"]
+ +

Check the dart-sass releases page for a newer version and swap both occurrences of 1.101.0 in the download URL if you want to track latest instead of matching this environment.

+ +

Build it once, then run it the same way you'd run the local sass CLI (skip the leading sass in the command — the image's ENTRYPOINT already supplies it):

+ +
docker build -t novaconium-sass -f Dockerfile .
+docker run --rm -v "$(pwd):/usr/src/app" -w /usr/src/app novaconium-sass \
+    --load-path=App/sass --load-path=novaconium/sass/defaults novaconium/sass/main.sass public/css/main.css
+ +

See git.4lt.ca/4lt/novaconium/docs/Sass.md for this project's own write-up of the Docker approach in general; the Dockerfile and paths/flags above are this project's own, adjusted to use Dart Sass directly and this repo's current layout.

+ +

Overriding just the colors

+ +

novaconium/sass/main.sass starts with @use 'colors' as *, but its own directory has no _colors.sass of its own — on purpose, so that @use falls through to the Sass load path above rather than resolving to a sibling file. App/sass/_colors.sass is checked first; novaconium/sass/defaults/_colors.sass (the framework's own palette) is the fallback. Same override-by-presence mechanism as App/pages/ over novaconium/pages/, just applied to Sass instead of Twig/PHP.

+ +

To reskin the whole site, edit the seven variables in App/sass/_colors.sass$bg, $surface, $text-color, $muted-color, $border-color, $accent, $accent-hover — and recompile. Nothing under novaconium/ needs to change. Delete App/sass/_colors.sass entirely to fall back to the framework's default palette instead.

+ +

Dark/light theme toggle

+ +

Every color rule in main.sass reads a CSS custom property (var(--bg), var(--accent), etc.) instead of a Sass variable directly. The Sass variables only seed the initial :root values at compile time; a :root[data-theme="light"] block overrides all seven using -light-suffixed variables from the same _colors.sass files ($bg-light, $surface-light, etc.) — same override mechanism, same files, a second palette.

+ +

The toggle button in novaconium/pages/_layout/nav.twig flips a data-theme attribute on <html> at runtime and persists the choice to localStorage. novaconium/pages/_layout/theme-init.twig, included early in <head> before the stylesheet, re-applies a saved choice before first paint on every later page load, so switching to light doesn't flash dark first. The sun/moon icon swap inside the button is pure CSS reacting to the attribute — no JS involved there — so it works correctly even on sidecar-less pages that get statically cached.

+ +

To customize the light theme the same way you'd customize the dark one, edit the -light variables in App/sass/_colors.sass and recompile.

+{% endblock %} diff --git a/novaconium/pages/admin/docs/third-party/index.twig b/novaconium/pages/admin/docs/third-party/index.twig new file mode 100644 index 0000000..297104f --- /dev/null +++ b/novaconium/pages/admin/docs/third-party/index.twig @@ -0,0 +1,13 @@ +{% extends 'admin/docs/_layout/layout.twig' %} + +{% block title %}Third-party{% endblock %} + +{% block description %}Vendored Twig and its license.{% endblock %} + +{% block robots %}noindex, nofollow{% endblock %} + +{% block docs_content %} +

Third-party

+ +

Twig is vendored in source form under novaconium/vendor/twig/ (no Composer — see Upgrading Twig for how to upgrade it). It's BSD-3-Clause licensed; the full license text ships alongside it at novaconium/vendor/twig/LICENSE.

+{% endblock %} diff --git a/novaconium/pages/admin/docs/upgrading-twig/index.twig b/novaconium/pages/admin/docs/upgrading-twig/index.twig new file mode 100644 index 0000000..bd56460 --- /dev/null +++ b/novaconium/pages/admin/docs/upgrading-twig/index.twig @@ -0,0 +1,44 @@ +{% extends 'admin/docs/_layout/layout.twig' %} + +{% block title %}Upgrading Twig{% endblock %} + +{% block description %}How to bump the vendored copy of Twig.{% endblock %} + +{% block robots %}noindex, nofollow{% endblock %} + +{% block docs_content %} +

Upgrading vendored Twig

+ +

This project has no Composer — Twig is vendored by hand as source files under novaconium/vendor/twig/. There's no lockfile and no composer.json, so upgrading is a manual copy-and-verify process rather than composer update.

+ +

What's currently vendored

+
    +
  • Version: 3.28.0 (see novaconium/vendor/twig/src/Environment.php, the Environment::VERSION constant — that constant is always the source of truth, this doc can drift).
  • +
  • Only the src/ directory from the Twig package is vendored — no tests, no docs, no composer.json. novaconium/autoload.php maps the Twig\ namespace straight at novaconium/vendor/twig/src/, so anything Twig autoloads has to live at the matching path under there.
  • +
  • novaconium/autoload.php also defines a trigger_deprecation() shim. Twig 3.x calls this function (normally supplied by symfony/deprecation-contracts, which isn't vendored here) whenever it hits a deprecated code path. Without the shim, upgrading to a Twig version that deprecates something this project uses would fatal instead of warn.
  • +
+ +

How to upgrade

+
    +
  1. Pick the target version from Twig's GitHub releases: https://github.com/twigphp/Twig/releases. Read the changelog for breaking changes, in particular anything touching Environment, Loader\FilesystemLoader, {% verbatim %}{% extends %}{% endverbatim %}/{% verbatim %}{% include %}{% endverbatim %} resolution, or autoescaping — those are the parts of Twig this project actually exercises (see novaconium/src/Renderer.php).
  2. +
  3. Download the release source (the "Source code (zip)" asset on the release page, or git clone --branch vX.Y.Z --depth 1 https://github.com/twigphp/Twig if you have network access to GitHub).
  4. +
  5. From the downloaded copy, take only the src/ directory.
  6. +
  7. Replace novaconium/vendor/twig/src/ wholesale with that new src/ directory (delete the old one first so removed files don't linger).
  8. +
  9. Copy the new LICENSE file over novaconium/vendor/twig/LICENSE too, in case it changed.
  10. +
  11. Confirm the namespace layout is unchanged: novaconium/autoload.php assumes Twig\Foo\Bar lives at src/Foo/Bar.php relative to the vendor root. This has been stable across Twig 3.x, but double-check if jumping a major version.
  12. +
  13. Run the app and click through every route (or run the manual checklist in the Design notes' Verification section) — there's no automated test suite, so this is the actual regression check: +
      +
    • /, /about — static pages, exercise {% verbatim %}{% extends layout %}{% endverbatim %}.
    • +
    • /blog and any post under it — a sidecar-driven listing plus sidecar-less posts.
    • +
    • A _layout/layout.twig overriding the root layout — exercises nested {% verbatim %}{% extends %}{% endverbatim %} resolution across the App/pages/ + novaconium/pages/ overlay (see novaconium/src/Overlay.php).
    • +
    • /contact — form handling, {% verbatim %}{% if %}{% endverbatim %} blocks, loop-free but exercises variable escaping ({% verbatim %}{{ old.name }}{% endverbatim %} etc.) — good smoke test for autoescape behavior changes.
    • +
    • /admin/docs and its subpages — pure Twig, no |raw — confirm they still render as expected after the upgrade.
    • +
    +
  14. +
  15. If PHP now emits E_USER_DEPRECATED warnings that weren't there before (visible because debug is true in novaconium/config.php), that's the trigger_deprecation() shim doing its job — read the message and update the calling code in novaconium/src/Renderer.php before the next major Twig version removes the deprecated path entirely.
  16. +
  17. Update the version note at the top of this page.
  18. +
+ +

Why not just use Composer?

+

The project intentionally has no build/install step — cloning the repo and pointing a web server at public/ is enough. That's a deliberate trade-off: upgrades are manual, but there's nothing to install, no lockfile to drift, and no vendor/ regeneration step for a fresh deploy.

+{% endblock %} diff --git a/novaconium/pages/admin/index.twig b/novaconium/pages/admin/index.twig new file mode 100644 index 0000000..002a59e --- /dev/null +++ b/novaconium/pages/admin/index.twig @@ -0,0 +1,21 @@ +{% extends layout %} + +{% import '_layout/icons.twig' as icons %} + +{% block title %}Admin{% endblock %} + +{% block description %}Site administration tools.{% endblock %} + +{% block robots %}noindex, nofollow{% endblock %} + +{% block content %} + +{% endblock %} diff --git a/novaconium/pages/admin/password-hash/index.php b/novaconium/pages/admin/password-hash/index.php new file mode 100644 index 0000000..eba1337 --- /dev/null +++ b/novaconium/pages/admin/password-hash/index.php @@ -0,0 +1,38 @@ + before hashing, producing a hash that + // doesn't match what's actually typed later at the Basic Auth prompt + // (which does zero sanitization). + $password = $_POST['password'] ?? ''; + + if ($password === '') { + $error = 'Enter a password to hash.'; + } elseif (strlen($password) < 8) { + $error = 'Use at least 8 characters.'; + } else { + // Computed once per request and never stored/logged — the page + // that renders this is the only place it's ever seen. + $hash = password_hash($password, PASSWORD_DEFAULT); + } +} + +return [ + 'hash' => $hash, + 'error' => $error, + 'securityError' => Input::get('error') === 'security', + 'csrfField' => Csrf::fieldName(), + 'csrfToken' => Csrf::token(), +]; diff --git a/novaconium/pages/admin/password-hash/index.twig b/novaconium/pages/admin/password-hash/index.twig new file mode 100644 index 0000000..1fdb801 --- /dev/null +++ b/novaconium/pages/admin/password-hash/index.twig @@ -0,0 +1,42 @@ +{% extends layout %} + +{% import '_layout/icons.twig' as icons %} + +{% block title %}Generate admin password hash{% endblock %} +{% block description %}Generate a password_hash() value for admin_password_hash without using the CLI.{% endblock %} +{% block robots %}noindex, nofollow{% endblock %} + +{% block content %} +
+

{{ icons.lock() }}Generate admin password hash

+ +

A browser-based alternative to php -r "echo password_hash('yourpassword', PASSWORD_DEFAULT);". Enter a password, get back the hash App/config.php expects for admin_password_hash — see {{ icons.book() }}Admin authentication. Nothing typed here is stored, logged, or sent anywhere except computed once for this response.

+ +
+ +

+
+ +

+ +
+ + {% if securityError %} +

Your session expired before submitting — please try again.

+ {% endif %} + + {% if error %} +

{{ error }}

+ {% endif %} + + {% if hash %} +

Add this to App/config.php:

+
<?php
+// App/config.php
+return [
+    'admin_username'      => 'admin',
+    'admin_password_hash' => '{{ hash }}',
+];
+ {% endif %} +
+{% endblock %} diff --git a/novaconium/sass/defaults/_colors.sass b/novaconium/sass/defaults/_colors.sass new file mode 100644 index 0000000..5d2566c --- /dev/null +++ b/novaconium/sass/defaults/_colors.sass @@ -0,0 +1,27 @@ +// Framework default color palette. Used only when a project doesn't +// supply App/sass/_colors.sass. novaconium/sass/main.sass does +// `@use 'colors' as *` — its own directory (novaconium/sass/) has no +// _colors.sass of its own on purpose, so that bare `@use` falls through +// to the Sass load path, compiled with App/sass first and this +// directory (novaconium/sass/defaults/) as the fallback: +// sass --load-path=App/sass --load-path=novaconium/sass/defaults \ +// novaconium/sass/main.sass public/css/main.css +// Not meant to be edited per-project; override by creating +// App/sass/_colors.sass with the same variable names instead. +$bg: #16181d +$surface: #1e2129 +$text-color: #e8eaed +$muted-color: #9099a6 +$border-color: #2b2f3a +$accent: #5b8cff +$accent-hover: #7ea1ff + +// Light theme counterpart, used when a visitor toggles it. Same naming +// convention as the dark set above: -light suffix, same seven names. +$bg-light: #ffffff +$surface-light: #eef1f6 +$text-color-light: #16181d +$muted-color-light: #5a6270 +$border-color-light: #d7dce3 +$accent-light: #3b6fe0 +$accent-hover-light: #2f5bc0 diff --git a/novaconium/sass/main.sass b/novaconium/sass/main.sass new file mode 100644 index 0000000..6f8c846 --- /dev/null +++ b/novaconium/sass/main.sass @@ -0,0 +1,392 @@ +// Colors resolve via the Sass load path: App/sass first, novaconium/sass +// as fallback (see novaconium/sass/defaults/_colors.sass and +// App/sass/_colors.sass). `as *` imports the variables unqualified, so +// they're used below only to seed the CSS custom properties in :root — +// every rule after that reads the color via var(--bg), var(--accent), +// etc., not the Sass variable directly. That indirection is what makes +// the dark/light theme toggle possible: novaconium/pages/_layout/theme- +// init.twig flips a data-theme attribute on at runtime (something +// Sass, which only runs at compile time, can't do on its own), and the +// second block below swaps every custom property in response. +@use 'colors' as * + +:root + --bg: #{$bg} + --surface: #{$surface} + --text-color: #{$text-color} + --muted-color: #{$muted-color} + --border-color: #{$border-color} + --accent: #{$accent} + --accent-hover: #{$accent-hover} + +:root[data-theme="light"] + --bg: #{$bg-light} + --surface: #{$surface-light} + --text-color: #{$text-color-light} + --muted-color: #{$muted-color-light} + --border-color: #{$border-color-light} + --accent: #{$accent-light} + --accent-hover: #{$accent-hover-light} + +$max-width: 40rem + +body + font-family: -apple-system, sans-serif + background: var(--bg) + color: var(--text-color) + max-width: $max-width + margin: 2rem auto + padding: 0 1rem + line-height: 1.6 + +a + color: var(--accent) + text-decoration: none + + &:hover + color: var(--accent-hover) + text-decoration: underline + +h1, h2, h3, h4, h5, h6 + color: var(--text-color) + line-height: 1.3 + +blockquote + margin: 1.5rem 0 + padding: 0.25rem 0 0.25rem 1rem + border-left: 3px solid var(--accent) + color: var(--muted-color) + font-style: italic + +table + width: 100% + margin: 1.5rem 0 + border-collapse: collapse + +th, td + padding: 0.5rem 0.75rem + border: 1px solid var(--border-color) + text-align: left + +th + color: var(--text-color) + font-weight: 600 + +img + max-width: 100% + height: auto + border-radius: 6px + +.icon + flex-shrink: 0 + +.icon-link + display: inline-flex + align-items: center + gap: 0.35rem + +.icon-heading + display: inline-flex + align-items: center + gap: 0.5rem + +nav + margin-bottom: 2rem + padding-bottom: 1rem + border-bottom: 1px solid var(--border-color) + + a + display: inline-flex + align-items: center + gap: 0.35rem + margin-right: 1rem + font-weight: 600 + +.theme-toggle + background: none + border: none + color: inherit + font-weight: normal + padding: 0 + cursor: pointer + + &:hover + background: none + + .icon-moon + display: none + +:root[data-theme="light"] .theme-toggle + .icon-sun + display: none + + .icon-moon + display: inline + +code + background: var(--surface) + color: var(--accent) + padding: 0.15em 0.4em + border-radius: 4px + font-size: 0.9em + +pre + background: var(--surface) + border: 1px solid var(--border-color) + border-radius: 6px + padding: 1rem + overflow-x: auto + + code + background: none + padding: 0 + color: var(--text-color) + +hr + border: none + border-top: 1px solid var(--border-color) + margin: 2rem 0 + +ul, ol + padding-left: 1.25rem + +label + color: var(--muted-color) + +small + color: var(--muted-color) + +// Honeypot field for spam prevention (see App/pages/contact/index.php). +// Off-screen positioning rather than display:none/visibility:hidden, +// since some spam bots specifically skip fields hidden that way. +.hp-field + position: absolute + left: -9999px + width: 1px + height: 1px + overflow: hidden + +button, select, textarea, input:not([type="radio"]):not([type="checkbox"]) + font-family: inherit + font-size: 1rem + background: var(--surface) + color: var(--text-color) + border: 1px solid var(--border-color) + border-radius: 6px + padding: 0.5rem 0.75rem + +select + cursor: pointer + +input[type="radio"], input[type="checkbox"] + accent-color: var(--accent) + margin-right: 0.35rem + vertical-align: middle + +button + background: var(--accent) + color: var(--bg) + border: none + font-weight: 600 + cursor: pointer + + &:hover + background: var(--accent-hover) + +.blog-layout + display: flex + gap: 2rem + + aside + color: var(--muted-color) + flex: 0 0 8rem + + article + flex: 1 + +@keyframes fade-in-up + from + opacity: 0 + transform: translateY(0.75rem) + to + opacity: 1 + transform: translateY(0) + +@keyframes glow-drift + 0% + transform: translate(-50%, -50%) rotate(0deg) + 100% + transform: translate(-50%, -50%) rotate(360deg) + +.hero + position: relative + padding: 3rem 0 2.5rem + text-align: center + overflow: hidden + + &::before + content: "" + position: absolute + top: 50% + left: 50% + width: 40rem + height: 40rem + background: conic-gradient(from 0deg, transparent 0deg, rgba(45, 212, 191, 0.16) 90deg, transparent 180deg) + animation: glow-drift 18s linear infinite + pointer-events: none + z-index: 0 + + > * + position: relative + z-index: 1 + animation: fade-in-up 0.6s ease-out both + + h1 + font-size: 2.5rem + margin: 0.5rem 0 1rem + animation-delay: 0.08s + + .hero-lede + animation-delay: 0.16s + + .hero-actions + animation-delay: 0.24s + + .hero-badges + animation-delay: 0.32s + +.hero-eyebrow + display: inline-block + color: var(--accent) + font-weight: 700 + letter-spacing: 0.08em + text-transform: uppercase + font-size: 0.85rem + +.hero-lede + max-width: 36rem + margin: 0 auto + color: var(--muted-color) + font-size: 1.05rem + +.hero-actions + display: flex + justify-content: center + gap: 1rem + margin-top: 1.75rem + +.button-link + display: inline-flex + align-items: center + gap: 0.4rem + background: var(--accent) + color: var(--bg) + font-weight: 600 + border-radius: 6px + padding: 0.65rem 1.25rem + text-decoration: none + + &:hover + background: var(--accent-hover) + color: var(--bg) + text-decoration: none + + &.button-link--ghost + background: transparent + color: var(--text-color) + border: 1px solid var(--border-color) + + &:hover + background: var(--surface) + color: var(--text-color) + +.hero-badges + display: flex + flex-wrap: wrap + justify-content: center + gap: 0.5rem + margin: 1.5rem 0 0 + padding: 0 + list-style: none + +.badge + background: var(--surface) + border: 1px solid var(--border-color) + color: var(--muted-color) + border-radius: 999px + padding: 0.3rem 0.85rem + font-size: 0.8rem + font-weight: 600 + +.feature-grid + display: grid + grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)) + gap: 1.25rem + margin: 2.5rem 0 + +.feature-card + background: var(--surface) + border: 1px solid var(--border-color) + border-radius: 8px + padding: 1.25rem 1.5rem + opacity: 0 + animation: fade-in-up 0.5s ease-out forwards + + @for $i from 1 through 6 + &:nth-child(#{$i}) + animation-delay: #{0.3 + $i * 0.06}s + + h2 + font-size: 1.1rem + margin: 0 0 0.5rem + + p + color: var(--muted-color) + margin: 0 + font-size: 0.95rem + +@media (prefers-reduced-motion: reduce) + .hero::before + animation: none + + .hero > *, .feature-card + animation: none + opacity: 1 + transform: none + +.next-steps + border-top: 1px solid var(--border-color) + padding-top: 2rem + margin-top: 1rem + + ul + padding-left: 1.25rem + +.post-list + list-style: none + padding: 0 + + li + padding: 1rem 0 + border-bottom: 1px solid var(--border-color) + + &:last-child + border-bottom: none + + h2 + margin: 0 0 0.35rem + font-size: 1.15rem + + p + color: var(--muted-color) + margin: 0 + +.footnotes + border-top: 1px solid var(--border-color) + margin-top: 2.5rem + padding-top: 1rem + font-size: 0.9rem + color: var(--muted-color) + + ol + padding-left: 1.25rem diff --git a/novaconium/src/AdminAuth.php b/novaconium/src/AdminAuth.php new file mode 100644 index 0000000..e3c31f7 --- /dev/null +++ b/novaconium/src/AdminAuth.php @@ -0,0 +1,63 @@ +You have been logged out. Return home.

'; + exit; + } +} diff --git a/novaconium/src/Cache.php b/novaconium/src/Cache.php new file mode 100644 index 0000000..4329aed --- /dev/null +++ b/novaconium/src/Cache.php @@ -0,0 +1,67 @@ + public/cache/blog/hello/index.html. Apache's .htaccess serves these + * directly, bypassing PHP entirely, when present. + */ +final class Cache +{ + public function __construct(private readonly string $cacheDir) + { + } + + public function path(string $requestUri): string + { + $path = trim(strtok($requestUri, '?') ?: '/', '/'); + $suffix = $path === '' ? '' : '/' . $path; + + return rtrim($this->cacheDir, '/') . $suffix . '/index.html'; + } + + public function write(string $requestUri, string $html): void + { + $file = $this->path($requestUri); + $dir = dirname($file); + + if (!is_dir($dir)) { + mkdir($dir, 0775, true); + } + + file_put_contents($file, $html); + } + + /** + * Deletes every cached page, leaving the cache directory itself in place. + */ + public function clear(): void + { + $dir = rtrim($this->cacheDir, '/'); + + if (!is_dir($dir)) { + return; + } + + $this->clearDir($dir); + } + + private function clearDir(string $dir): void + { + foreach (scandir($dir) ?: [] as $entry) { + if ($entry === '.' || $entry === '..') { + continue; + } + + $path = $dir . '/' . $entry; + + if (is_dir($path)) { + $this->clearDir($path); + rmdir($path); + } else { + unlink($path); + } + } + } +} diff --git a/novaconium/src/Overlay.php b/novaconium/src/Overlay.php new file mode 100644 index 0000000..222a515 --- /dev/null +++ b/novaconium/src/Overlay.php @@ -0,0 +1,76 @@ +pagesDirs); + $this->twig = new Environment($loader, [ + 'cache' => false, + ]); + $this->twig->addGlobal('admin_auth_enabled', $adminAuthEnabled); + $this->twig->addGlobal('matomo_url', $matomoUrl); + $this->twig->addGlobal('matomo_site_id', $matomoSiteId); + $this->twig->addGlobal('site_name', $siteName); + $this->twig->addGlobal('is_404', false); + } + + public function render(Route $route, string $requestUri): void + { + $sidecarRel = $this->withFile($route->dir, 'index.php'); + $sidecar = Overlay::findFile($this->pagesDirs, $sidecarRel); + $hasSidecar = $sidecar !== null; + + $result = $hasSidecar ? $this->runSidecar($sidecar, $route->params) : []; + + if ($result instanceof Response) { + $result->emit(); + return; + } + + $data = array_merge((array) $result, ['params' => $route->params]); + $data['layout'] = $this->relativeLayoutPath($route->dir); + $data['request_path'] = parse_url($requestUri, PHP_URL_PATH) ?: '/'; + + $templateName = $this->withFile($route->dir, 'index.twig'); + $html = $this->twig->render($templateName, $data); + + http_response_code(200); + header('Content-Type: text/html; charset=utf-8'); + echo $html; + + if (!$hasSidecar) { + $this->cache->write($requestUri, $html); + } + } + + public function renderNotFound(string $requestUri): void + { + http_response_code(404); + header('Content-Type: text/html; charset=utf-8'); + + if (Overlay::isFile($this->pagesDirs, '404/index.twig')) { + echo $this->twig->render('404/index.twig', [ + 'layout' => $this->relativeLayoutPath('404'), + 'request_path' => parse_url($requestUri, PHP_URL_PATH) ?: '/', + 'is_404' => true, + ]); + return; + } + + echo '404 Not Found'; + } + + /** + * $cache is exposed to every sidecar's scope (e.g. to call + * $cache->clear() from an admin action) alongside $params. + */ + private function runSidecar(string $file, array $params): mixed + { + return (static function (string $__file, array $params, Cache $cache) { + return require $__file; + })($file, $params, $this->cache); + } + + private function withFile(string $dir, string $file): string + { + return $dir === '' ? $file : $dir . '/' . $file; + } + + /** + * Walk upward from $dir looking for the nearest _layout/layout.twig, + * falling back to the root _layout/layout.twig. + */ + private function relativeLayoutPath(string $dir): ?string + { + $current = $dir; + + while (true) { + $candidate = $this->withFile($current, '_layout/layout.twig'); + if (Overlay::isFile($this->pagesDirs, $candidate)) { + return $candidate; + } + + if ($current === '') { + return null; + } + + $slash = strrpos($current, '/'); + $current = $slash === false ? '' : substr($current, 0, $slash); + } + } +} diff --git a/novaconium/src/Response.php b/novaconium/src/Response.php new file mode 100644 index 0000000..cdf2d4b --- /dev/null +++ b/novaconium/src/Response.php @@ -0,0 +1,59 @@ + 'application/json']); + } + + public static function xml(string $xml, int $status = 200): self + { + return new self('xml', $xml, $status, ['Content-Type' => 'application/xml']); + } + + public static function html(string $html, int $status = 200): self + { + return new self('html', $html, $status, ['Content-Type' => 'text/html']); + } + + public function emit(): void + { + http_response_code($this->status); + + foreach ($this->headers as $name => $value) { + header("$name: $value"); + } + + if ($this->type === 'redirect') { + header('Location: ' . $this->body); + return; + } + + if ($this->type === 'json') { + echo json_encode($this->body, JSON_THROW_ON_ERROR); + return; + } + + echo $this->body; + } +} diff --git a/novaconium/src/Route.php b/novaconium/src/Route.php new file mode 100644 index 0000000..8b98fec --- /dev/null +++ b/novaconium/src/Route.php @@ -0,0 +1,26 @@ + $params + */ + public function __construct( + public readonly ?string $dir, + public readonly array $params, + public readonly bool $found, + ) { + } + + public static function notFound(): self + { + return new self(null, [], false); + } +} diff --git a/novaconium/src/Router.php b/novaconium/src/Router.php new file mode 100644 index 0000000..e051609 --- /dev/null +++ b/novaconium/src/Router.php @@ -0,0 +1,57 @@ + $s !== '')); + + $dir = ''; + $params = []; + + foreach ($segments as $segment) { + if (str_starts_with($segment, '_') || $segment === '404') { + // Reserved segments (e.g. "_layout", "404") are never routable directly. + return Route::notFound(); + } + + $exact = $dir === '' ? $segment : $dir . '/' . $segment; + if (Overlay::isDir($this->pagesDirs, $exact)) { + $dir = $exact; + continue; + } + + $wildcard = Overlay::findWildcardDir($this->pagesDirs, $dir); + if ($wildcard === null) { + return Route::notFound(); + } + + $paramName = trim($wildcard, '[]'); + $params[$paramName] = $segment; + $dir = $dir === '' ? $wildcard : $dir . '/' . $wildcard; + } + + // A page needs at least a template or a sidecar (e.g. a JSON-only + // API endpoint may have no index.twig at all). + if (!Overlay::isFile($this->pagesDirs, $dir . '/index.twig') && !Overlay::isFile($this->pagesDirs, $dir . '/index.php')) { + return Route::notFound(); + } + + return new Route($dir, $params, true); + } +} diff --git a/novaconium/vendor/twig/LICENSE b/novaconium/vendor/twig/LICENSE new file mode 100644 index 0000000..fd8234e --- /dev/null +++ b/novaconium/vendor/twig/LICENSE @@ -0,0 +1,27 @@ +Copyright (c) 2009-present by the Twig Team. + +All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + * Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + * Neither the name of Twig nor the names of its contributors + may be used to endorse or promote products derived from this software + without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/novaconium/vendor/twig/src/AbstractTwigCallable.php b/novaconium/vendor/twig/src/AbstractTwigCallable.php new file mode 100644 index 0000000..4ea9f66 --- /dev/null +++ b/novaconium/vendor/twig/src/AbstractTwigCallable.php @@ -0,0 +1,199 @@ + + */ +abstract class AbstractTwigCallable implements TwigCallableInterface +{ + protected $options; + + private $name; + private $dynamicName; + private $callable; + private $arguments; + + public function __construct(string $name, $callable = null, array $options = []) + { + $this->name = $this->dynamicName = $name; + $this->callable = $callable; + $this->arguments = []; + $this->options = array_merge([ + 'needs_environment' => false, + 'needs_context' => false, + 'needs_charset' => false, + 'needs_is_sandboxed' => false, + 'is_variadic' => false, + 'always_allowed_in_sandbox' => false, + 'deprecation_info' => null, + 'deprecated' => false, + 'deprecating_package' => '', + 'alternative' => null, + ], $options); + + if ($this->options['deprecation_info'] && !$this->options['deprecation_info'] instanceof DeprecatedCallableInfo) { + throw new \LogicException(\sprintf('The "deprecation_info" option must be an instance of "%s".', DeprecatedCallableInfo::class)); + } + + if ($this->options['deprecated']) { + if ($this->options['deprecation_info']) { + throw new \LogicException('When setting the "deprecation_info" option, you need to remove the obsolete deprecated options.'); + } + + trigger_deprecation('twig/twig', '3.15', 'Using the "deprecated", "deprecating_package", and "alternative" options is deprecated, pass a "deprecation_info" one instead.'); + + $this->options['deprecation_info'] = new DeprecatedCallableInfo( + $this->options['deprecating_package'], + $this->options['deprecated'], + null, + $this->options['alternative'], + ); + } + + if ($this->options['deprecation_info']) { + $this->options['deprecation_info']->setName($name); + $this->options['deprecation_info']->setType($this->getType()); + } + } + + public function __toString(): string + { + return \sprintf('%s(%s)', static::class, $this->name); + } + + public function getName(): string + { + return $this->name; + } + + public function getDynamicName(): string + { + return $this->dynamicName; + } + + /** + * @return callable|array{class-string, string}|null + */ + public function getCallable() + { + return $this->callable; + } + + public function getNodeClass(): string + { + return $this->options['node_class']; + } + + public function needsCharset(): bool + { + return $this->options['needs_charset']; + } + + public function needsEnvironment(): bool + { + return $this->options['needs_environment']; + } + + public function needsContext(): bool + { + return $this->options['needs_context']; + } + + public function needsIsSandboxed(): bool + { + return $this->options['needs_is_sandboxed']; + } + + public function isAlwaysAllowedInSandbox(): bool + { + return $this->options['always_allowed_in_sandbox']; + } + + /** + * @return static + */ + public function withDynamicArguments(string $name, string $dynamicName, array $arguments): self + { + $new = clone $this; + $new->name = $name; + $new->dynamicName = $dynamicName; + $new->arguments = $arguments; + + return $new; + } + + /** + * @deprecated since Twig 3.12, use withDynamicArguments() instead + */ + public function setArguments(array $arguments): void + { + trigger_deprecation('twig/twig', '3.12', 'The "%s::setArguments()" method is deprecated, use "%s::withDynamicArguments()" instead.', static::class, static::class); + + $this->arguments = $arguments; + } + + public function getArguments(): array + { + return $this->arguments; + } + + public function isVariadic(): bool + { + return $this->options['is_variadic']; + } + + public function isDeprecated(): bool + { + return (bool) $this->options['deprecation_info']; + } + + public function triggerDeprecation(?string $file = null, ?int $line = null): void + { + $this->options['deprecation_info']->triggerDeprecation($file, $line); + } + + /** + * @deprecated since Twig 3.15 + */ + public function getDeprecatingPackage(): string + { + trigger_deprecation('twig/twig', '3.15', 'The "%s" method is deprecated, use "%s::triggerDeprecation()" instead.', __METHOD__, static::class); + + return $this->options['deprecating_package']; + } + + /** + * @deprecated since Twig 3.15 + */ + public function getDeprecatedVersion(): string + { + trigger_deprecation('twig/twig', '3.15', 'The "%s" method is deprecated, use "%s::triggerDeprecation()" instead.', __METHOD__, static::class); + + return \is_bool($this->options['deprecated']) ? '' : $this->options['deprecated']; + } + + /** + * @deprecated since Twig 3.15 + */ + public function getAlternative(): ?string + { + trigger_deprecation('twig/twig', '3.15', 'The "%s" method is deprecated, use "%s::triggerDeprecation()" instead.', __METHOD__, static::class); + + return $this->options['alternative']; + } + + public function getMinimalNumberOfRequiredArguments(): int + { + return ($this->options['needs_charset'] ? 1 : 0) + ($this->options['needs_environment'] ? 1 : 0) + ($this->options['needs_context'] ? 1 : 0) + ($this->options['needs_is_sandboxed'] ? 1 : 0) + \count($this->arguments); + } +} diff --git a/novaconium/vendor/twig/src/Attribute/AsTwigFilter.php b/novaconium/vendor/twig/src/Attribute/AsTwigFilter.php new file mode 100644 index 0000000..5689c9d --- /dev/null +++ b/novaconium/vendor/twig/src/Attribute/AsTwigFilter.php @@ -0,0 +1,60 @@ + + */ +interface CacheInterface +{ + /** + * Generates a cache key for the given template class name. + */ + public function generateKey(string $name, string $className): string; + + /** + * Writes the compiled template to cache. + * + * @param string $content The template representation as a PHP class + */ + public function write(string $key, string $content): void; + + /** + * Loads a template from the cache. + */ + public function load(string $key): void; + + /** + * Returns the modification timestamp of a key. + */ + public function getTimestamp(string $key): int; +} diff --git a/novaconium/vendor/twig/src/Cache/ChainCache.php b/novaconium/vendor/twig/src/Cache/ChainCache.php new file mode 100644 index 0000000..1c2098f --- /dev/null +++ b/novaconium/vendor/twig/src/Cache/ChainCache.php @@ -0,0 +1,88 @@ + + */ +final class ChainCache implements CacheInterface, RemovableCacheInterface +{ + /** + * @param iterable $caches The ordered list of caches used to store and fetch cached items + */ + public function __construct( + private iterable $caches, + ) { + } + + public function generateKey(string $name, string $className): string + { + return $className.'#'.$name; + } + + public function write(string $key, string $content): void + { + $splitKey = $this->splitKey($key); + + foreach ($this->caches as $cache) { + $cache->write($cache->generateKey(...$splitKey), $content); + } + } + + public function load(string $key): void + { + [$name, $className] = $this->splitKey($key); + + foreach ($this->caches as $cache) { + $cache->load($cache->generateKey($name, $className)); + + if (class_exists($className, false)) { + break; + } + } + } + + public function getTimestamp(string $key): int + { + $splitKey = $this->splitKey($key); + + foreach ($this->caches as $cache) { + if (0 < $timestamp = $cache->getTimestamp($cache->generateKey(...$splitKey))) { + return $timestamp; + } + } + + return 0; + } + + public function remove(string $name, string $cls): void + { + foreach ($this->caches as $cache) { + if ($cache instanceof RemovableCacheInterface) { + $cache->remove($name, $cls); + } + } + } + + /** + * @return string[] + */ + private function splitKey(string $key): array + { + return array_reverse(explode('#', $key, 2)); + } +} diff --git a/novaconium/vendor/twig/src/Cache/FilesystemCache.php b/novaconium/vendor/twig/src/Cache/FilesystemCache.php new file mode 100644 index 0000000..5840585 --- /dev/null +++ b/novaconium/vendor/twig/src/Cache/FilesystemCache.php @@ -0,0 +1,95 @@ + + */ +class FilesystemCache implements CacheInterface, RemovableCacheInterface +{ + public const FORCE_BYTECODE_INVALIDATION = 1; + + private $directory; + private $options; + + public function __construct(string $directory, int $options = 0) + { + $this->directory = rtrim($directory, '\/').'/'; + $this->options = $options; + } + + public function generateKey(string $name, string $className): string + { + $hash = hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $className); + + return $this->directory.$hash[0].$hash[1].'/'.$hash.'.php'; + } + + public function load(string $key): void + { + if (is_file($key)) { + @include_once $key; + } + } + + public function write(string $key, string $content): void + { + $dir = \dirname($key); + if (!is_dir($dir)) { + if (false === @mkdir($dir, 0777, true)) { + clearstatcache(true, $dir); + if (!is_dir($dir)) { + throw new \RuntimeException(\sprintf('Unable to create the cache directory (%s).', $dir)); + } + } + } elseif (!is_writable($dir)) { + throw new \RuntimeException(\sprintf('Unable to write in the cache directory (%s).', $dir)); + } + + $tmpFile = tempnam($dir, basename($key)); + if (false !== @file_put_contents($tmpFile, $content) && @rename($tmpFile, $key)) { + @chmod($key, 0666 & ~umask()); + + if (self::FORCE_BYTECODE_INVALIDATION == ($this->options & self::FORCE_BYTECODE_INVALIDATION)) { + // Compile cached file into bytecode cache + if (\function_exists('opcache_invalidate') && filter_var(\ini_get('opcache.enable'), \FILTER_VALIDATE_BOOLEAN)) { + @opcache_invalidate($key, true); + } elseif (\function_exists('apc_compile_file')) { + apc_compile_file($key); + } + } + + return; + } + + throw new \RuntimeException(\sprintf('Failed to write cache file "%s".', $key)); + } + + public function remove(string $name, string $cls): void + { + $key = $this->generateKey($name, $cls); + if (!@unlink($key) && file_exists($key)) { + throw new \RuntimeException(\sprintf('Failed to delete cache file "%s".', $key)); + } + } + + public function getTimestamp(string $key): int + { + if (!is_file($key)) { + return 0; + } + + return (int) @filemtime($key); + } +} diff --git a/novaconium/vendor/twig/src/Cache/NullCache.php b/novaconium/vendor/twig/src/Cache/NullCache.php new file mode 100644 index 0000000..1ae2169 --- /dev/null +++ b/novaconium/vendor/twig/src/Cache/NullCache.php @@ -0,0 +1,42 @@ + + */ +final class NullCache implements CacheInterface, RemovableCacheInterface +{ + public function generateKey(string $name, string $className): string + { + return ''; + } + + public function write(string $key, string $content): void + { + } + + public function load(string $key): void + { + } + + public function getTimestamp(string $key): int + { + return 0; + } + + public function remove(string $name, string $cls): void + { + } +} diff --git a/novaconium/vendor/twig/src/Cache/ReadOnlyFilesystemCache.php b/novaconium/vendor/twig/src/Cache/ReadOnlyFilesystemCache.php new file mode 100644 index 0000000..3ba6514 --- /dev/null +++ b/novaconium/vendor/twig/src/Cache/ReadOnlyFilesystemCache.php @@ -0,0 +1,25 @@ + + */ +class ReadOnlyFilesystemCache extends FilesystemCache +{ + public function write(string $key, string $content): void + { + // Do nothing with the content, it's a read-only filesystem. + } +} diff --git a/novaconium/vendor/twig/src/Cache/RemovableCacheInterface.php b/novaconium/vendor/twig/src/Cache/RemovableCacheInterface.php new file mode 100644 index 0000000..05da569 --- /dev/null +++ b/novaconium/vendor/twig/src/Cache/RemovableCacheInterface.php @@ -0,0 +1,20 @@ + + */ +interface RemovableCacheInterface +{ + public function remove(string $name, string $cls): void; +} diff --git a/novaconium/vendor/twig/src/Compiler.php b/novaconium/vendor/twig/src/Compiler.php new file mode 100644 index 0000000..976030f --- /dev/null +++ b/novaconium/vendor/twig/src/Compiler.php @@ -0,0 +1,263 @@ + + */ +class Compiler +{ + private $lastLine; + private $source; + private $indentation; + private $debugInfo = []; + private $sourceOffset; + private $sourceLine; + private $varNameSalt = 0; + private $didUseEcho = false; + private $didUseEchoStack = []; + + public function __construct( + private Environment $env, + ) { + } + + public function getEnvironment(): Environment + { + return $this->env; + } + + public function getSource(): string + { + return $this->source; + } + + /** + * @return $this + */ + public function reset(int $indentation = 0) + { + $this->lastLine = null; + $this->source = ''; + $this->debugInfo = []; + $this->sourceOffset = 0; + // source code starts at 1 (as we then increment it when we encounter new lines) + $this->sourceLine = 1; + $this->indentation = $indentation; + $this->varNameSalt = 0; + + return $this; + } + + /** + * @return $this + */ + public function compile(Node $node, int $indentation = 0) + { + $this->reset($indentation); + $this->didUseEchoStack[] = $this->didUseEcho; + + try { + $this->didUseEcho = false; + $node->compile($this); + + if ($this->didUseEcho) { + trigger_deprecation('twig/twig', '3.9', 'Using "%s" is deprecated, use "yield" instead in "%s", then flag the class with #[\Twig\Attribute\YieldReady].', $this->didUseEcho, $node::class); + } + + return $this; + } finally { + $this->didUseEcho = array_pop($this->didUseEchoStack); + } + } + + /** + * @return $this + */ + public function subcompile(Node $node, bool $raw = true) + { + if (!$raw) { + $this->source .= str_repeat(' ', $this->indentation * 4); + } + + $this->didUseEchoStack[] = $this->didUseEcho; + + try { + $this->didUseEcho = false; + $node->compile($this); + + if ($this->didUseEcho) { + trigger_deprecation('twig/twig', '3.9', 'Using "%s" is deprecated, use "yield" instead in "%s", then flag the class with #[\Twig\Attribute\YieldReady].', $this->didUseEcho, $node::class); + } + + return $this; + } finally { + $this->didUseEcho = array_pop($this->didUseEchoStack); + } + } + + /** + * Adds a raw string to the compiled code. + * + * @return $this + */ + public function raw(string $string) + { + $this->checkForEcho($string); + $this->source .= $string; + + return $this; + } + + /** + * Writes a string to the compiled code by adding indentation. + * + * @return $this + */ + public function write(...$strings) + { + foreach ($strings as $string) { + $this->checkForEcho($string); + $this->source .= str_repeat(' ', $this->indentation * 4).$string; + } + + return $this; + } + + /** + * Adds a quoted string to the compiled code. + * + * @return $this + */ + public function string(string $value) + { + // Single quotes are encoded as \x27 (not \') as a defense-in-depth measure: + // it guarantees that the compiled output never contains a literal "'" derived + // from user input, which prevents breaking out of a surrounding single-quoted + // PHP context if a caller mistakenly concatenates the result into one. + // \' is not a recognized escape sequence in PHP double-quoted strings (the + // backslash would be kept literally), so \x27 is used instead. + $this->source .= \sprintf('"%s"', str_replace("'", '\\x27', addcslashes($value, "\0\t\"\$\\"))); + + return $this; + } + + /** + * Returns a PHP representation of a given value. + * + * @return $this + */ + public function repr($value) + { + if (\is_int($value) || \is_float($value)) { + if (false !== $locale = setlocale(\LC_NUMERIC, '0')) { + setlocale(\LC_NUMERIC, 'C'); + } + + $this->raw(var_export($value, true)); + + if (false !== $locale) { + setlocale(\LC_NUMERIC, $locale); + } + } elseif (null === $value) { + $this->raw('null'); + } elseif (\is_bool($value)) { + $this->raw($value ? 'true' : 'false'); + } elseif (\is_array($value)) { + $this->raw('['); + $first = true; + foreach ($value as $key => $v) { + if (!$first) { + $this->raw(', '); + } + $first = false; + $this->repr($key); + $this->raw(' => '); + $this->repr($v); + } + $this->raw(']'); + } else { + $this->string($value); + } + + return $this; + } + + /** + * @return $this + */ + public function addDebugInfo(Node $node) + { + if ($node->getTemplateLine() != $this->lastLine) { + $this->write(\sprintf("// line %d\n", $node->getTemplateLine())); + + $this->sourceLine += substr_count($this->source, "\n", $this->sourceOffset); + $this->sourceOffset = \strlen($this->source); + $this->debugInfo[$this->sourceLine] = $node->getTemplateLine(); + + $this->lastLine = $node->getTemplateLine(); + } + + return $this; + } + + public function getDebugInfo(): array + { + ksort($this->debugInfo); + + return $this->debugInfo; + } + + /** + * @return $this + */ + public function indent(int $step = 1) + { + $this->indentation += $step; + + return $this; + } + + /** + * @return $this + * + * @throws \LogicException When trying to outdent too much so the indentation would become negative + */ + public function outdent(int $step = 1) + { + // can't outdent by more steps than the current indentation level + if ($this->indentation < $step) { + throw new \LogicException('Unable to call outdent() as the indentation would become negative.'); + } + + $this->indentation -= $step; + + return $this; + } + + public function getVarName(): string + { + return \sprintf('_v%d', $this->varNameSalt++); + } + + private function checkForEcho(string $string): void + { + if ($this->didUseEcho) { + return; + } + + $this->didUseEcho = preg_match('/^\s*+(echo|print)\b/', $string, $m) ? $m[1] : false; + } +} diff --git a/novaconium/vendor/twig/src/DeprecatedCallableInfo.php b/novaconium/vendor/twig/src/DeprecatedCallableInfo.php new file mode 100644 index 0000000..2db9f3d --- /dev/null +++ b/novaconium/vendor/twig/src/DeprecatedCallableInfo.php @@ -0,0 +1,67 @@ + + */ +final class DeprecatedCallableInfo +{ + private string $type; + private string $name; + + public function __construct( + private string $package, + private string $version, + private ?string $altName = null, + private ?string $altPackage = null, + private ?string $altVersion = null, + ) { + } + + public function setType(string $type): void + { + $this->type = $type; + } + + public function setName(string $name): void + { + $this->name = $name; + } + + public function triggerDeprecation(?string $file = null, ?int $line = null): void + { + $message = \sprintf('Twig %s "%s" is deprecated', ucfirst($this->type), $this->name); + + if ($this->altName) { + $message .= \sprintf('; use "%s"', $this->altName); + if ($this->altPackage) { + $message .= \sprintf(' from the "%s" package', $this->altPackage); + } + if ($this->altVersion) { + $message .= \sprintf(' (available since version %s)', $this->altVersion); + } + $message .= ' instead'; + } + + if ($file) { + $message .= \sprintf(' in %s', $file); + if ($line) { + $message .= \sprintf(' at line %d', $line); + } + } + + $message .= '.'; + + trigger_deprecation($this->package, $this->version, $message); + } +} diff --git a/novaconium/vendor/twig/src/Environment.php b/novaconium/vendor/twig/src/Environment.php new file mode 100644 index 0000000..2727997 --- /dev/null +++ b/novaconium/vendor/twig/src/Environment.php @@ -0,0 +1,953 @@ + + */ +class Environment +{ + public const VERSION = '3.28.0'; + public const VERSION_ID = 32800; + public const MAJOR_VERSION = 3; + public const MINOR_VERSION = 28; + public const RELEASE_VERSION = 0; + public const EXTRA_VERSION = ''; + + private $charset; + private $loader; + private $debug; + private $autoReload; + private $cache; + private $lexer; + private $parser; + private $compiler; + /** @var array */ + private $globals = []; + private $resolvedGlobals; + private $loadedTemplates; + private $strictVariables; + private $originalCache; + private $extensionSet; + private $runtimeLoaders = []; + private $runtimes = []; + private $optionsHash; + /** @var bool */ + private $useYield; + private $defaultRuntimeLoader; + private array $hotCache = []; + + /** + * Constructor. + * + * Available options: + * + * * debug: When set to true, it automatically set "auto_reload" to true as + * well (default to false). + * + * * charset: The charset used by the templates (default to UTF-8). + * + * * cache: An absolute path where to store the compiled templates, + * a \Twig\Cache\CacheInterface implementation, + * or false to disable compilation cache (default). + * + * * auto_reload: Whether to reload the template if the original source changed. + * If you don't provide the auto_reload option, it will be + * determined automatically based on the debug value. + * + * * strict_variables: Whether to ignore invalid variables in templates + * (default to false). + * + * * autoescape: Whether to enable auto-escaping (default to html): + * * false: disable auto-escaping + * * html, js: set the autoescaping to one of the supported strategies + * * name: set the autoescaping strategy based on the template name extension + * * PHP callback: a PHP callback that returns an escaping strategy based on the template "name" + * + * * optimizations: A flag that indicates which optimizations to apply + * (default to -1 which means that all optimizations are enabled; + * set it to 0 to disable). + * + * * use_yield: true: forces templates to exclusively use "yield" instead of "echo" (all extensions must be yield ready) + * false (default): allows templates to use a mix of "yield" and "echo" calls to allow for a progressive migration + * Switch to "true" when possible as this will be the only supported mode in Twig 4.0 + */ + public function __construct(LoaderInterface $loader, array $options = []) + { + $this->setLoader($loader); + + $options = array_merge([ + 'debug' => false, + 'charset' => 'UTF-8', + 'strict_variables' => false, + 'autoescape' => 'html', + 'cache' => false, + 'auto_reload' => null, + 'optimizations' => -1, + 'use_yield' => false, + ], $options); + + $this->useYield = (bool) $options['use_yield']; + $this->debug = (bool) $options['debug']; + $this->setCharset($options['charset'] ?? 'UTF-8'); + $this->autoReload = null === $options['auto_reload'] ? $this->debug : (bool) $options['auto_reload']; + $this->strictVariables = (bool) $options['strict_variables']; + $this->setCache($options['cache']); + $this->extensionSet = new ExtensionSet(); + $this->defaultRuntimeLoader = new FactoryRuntimeLoader([ + EscaperRuntime::class => function () { return new EscaperRuntime($this->charset); }, + ]); + + $this->addExtension(new CoreExtension()); + $escaperExt = new EscaperExtension($options['autoescape']); + $escaperExt->setEnvironment($this, false); + $this->addExtension($escaperExt); + if (\PHP_VERSION_ID >= 80000) { + $this->addExtension(new YieldNotReadyExtension($this->useYield)); + } + $this->addExtension(new OptimizerExtension($options['optimizations'])); + } + + /** + * @internal + */ + public function useYield(): bool + { + return $this->useYield; + } + + /** + * Enables debugging mode. + * + * @return void + */ + public function enableDebug() + { + $this->debug = true; + $this->updateOptionsHash(); + } + + /** + * Disables debugging mode. + * + * @return void + */ + public function disableDebug() + { + $this->debug = false; + $this->updateOptionsHash(); + } + + /** + * Checks if debug mode is enabled. + * + * @return bool true if debug mode is enabled, false otherwise + */ + public function isDebug() + { + return $this->debug; + } + + /** + * Enables the auto_reload option. + * + * @return void + */ + public function enableAutoReload() + { + $this->autoReload = true; + } + + /** + * Disables the auto_reload option. + * + * @return void + */ + public function disableAutoReload() + { + $this->autoReload = false; + } + + /** + * Checks if the auto_reload option is enabled. + * + * @return bool true if auto_reload is enabled, false otherwise + */ + public function isAutoReload() + { + return $this->autoReload; + } + + /** + * Enables the strict_variables option. + * + * @return void + */ + public function enableStrictVariables() + { + $this->strictVariables = true; + $this->updateOptionsHash(); + } + + /** + * Disables the strict_variables option. + * + * @return void + */ + public function disableStrictVariables() + { + $this->strictVariables = false; + $this->updateOptionsHash(); + } + + /** + * Checks if the strict_variables option is enabled. + * + * @return bool true if strict_variables is enabled, false otherwise + */ + public function isStrictVariables() + { + return $this->strictVariables; + } + + public function removeCache(string $name): void + { + $cls = $this->getTemplateClass($name); + $this->hotCache[$name] = $cls.'_'.bin2hex(random_bytes(16)); + + if ($this->cache instanceof RemovableCacheInterface) { + $this->cache->remove($name, $cls); + } else { + throw new \LogicException(\sprintf('The "%s" cache class does not support removing template cache as it does not implement the "RemovableCacheInterface" interface.', \get_class($this->cache))); + } + } + + /** + * Gets the current cache implementation. + * + * @param bool $original Whether to return the original cache option or the real cache instance + * + * @return CacheInterface|string|false A Twig\Cache\CacheInterface implementation, + * an absolute path to the compiled templates, + * or false to disable cache + */ + public function getCache($original = true) + { + return $original ? $this->originalCache : $this->cache; + } + + /** + * Sets the current cache implementation. + * + * @param CacheInterface|string|false $cache A Twig\Cache\CacheInterface implementation, + * an absolute path to the compiled templates, + * or false to disable cache + * + * @return void + */ + public function setCache($cache) + { + if (\is_string($cache)) { + $this->originalCache = $cache; + $this->cache = new FilesystemCache($cache, $this->autoReload ? FilesystemCache::FORCE_BYTECODE_INVALIDATION : 0); + } elseif (false === $cache) { + $this->originalCache = $cache; + $this->cache = new NullCache(); + } elseif ($cache instanceof CacheInterface) { + $this->originalCache = $this->cache = $cache; + } else { + throw new \LogicException('Cache can only be a string, false, or a \Twig\Cache\CacheInterface implementation.'); + } + } + + /** + * Gets the template class associated with the given string. + * + * The generated template class is based on the following parameters: + * + * * The cache key for the given template; + * * The currently enabled extensions; + * * PHP version; + * * Twig version; + * * Options with what environment was created. + * + * @param string $name The name for which to calculate the template class name + * @param int|null $index The index if it is an embedded template + * + * @internal + */ + public function getTemplateClass(string $name, ?int $index = null): string + { + $key = ($this->hotCache[$name] ?? $this->getLoader()->getCacheKey($name)).$this->optionsHash; + + return '__TwigTemplate_'.hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $key).(null === $index ? '' : '___'.$index); + } + + /** + * Renders a template. + * + * @param string|TemplateWrapper $name The template name + * + * @throws LoaderError When the template cannot be found + * @throws SyntaxError When an error occurred during compilation + * @throws RuntimeError When an error occurred during rendering + */ + public function render($name, array $context = []): string + { + return $this->load($name)->render($context); + } + + /** + * Displays a template. + * + * @param string|TemplateWrapper $name The template name + * + * @throws LoaderError When the template cannot be found + * @throws SyntaxError When an error occurred during compilation + * @throws RuntimeError When an error occurred during rendering + */ + public function display($name, array $context = []): void + { + $this->load($name)->display($context); + } + + /** + * Loads a template. + * + * @param string|TemplateWrapper $name The template name + * + * @throws LoaderError When the template cannot be found + * @throws RuntimeError When a previously generated cache is corrupted + * @throws SyntaxError When an error occurred during compilation + */ + public function load($name): TemplateWrapper + { + if ($name instanceof TemplateWrapper) { + return $name; + } + if ($name instanceof Template) { + trigger_deprecation('twig/twig', '3.9', 'Passing a "%s" instance to "%s" is deprecated.', self::class, __METHOD__); + + return $name; + } + + return new TemplateWrapper($this, $this->loadTemplate($this->getTemplateClass($name), $name)); + } + + /** + * Loads a template internal representation. + * + * This method is for internal use only and should never be called + * directly. + * + * @param string $name The template name + * @param int|null $index The index if it is an embedded template + * + * @throws LoaderError When the template cannot be found + * @throws RuntimeError When a previously generated cache is corrupted + * @throws SyntaxError When an error occurred during compilation + * + * @internal + */ + public function loadTemplate(string $cls, string $name, ?int $index = null): Template + { + $mainCls = $cls; + if (null !== $index) { + $cls .= '___'.$index; + } + + if (isset($this->loadedTemplates[$cls])) { + return $this->loadedTemplates[$cls]; + } + + if (!class_exists($cls, false)) { + $key = $this->cache->generateKey($name, $mainCls); + + if (!$this->isAutoReload() || $this->isTemplateFresh($name, $this->cache->getTimestamp($key))) { + $this->cache->load($key); + } + + if (!class_exists($cls, false)) { + $source = $this->getLoader()->getSourceContext($name); + $content = $this->compileSource($source); + if (!isset($this->hotCache[$name])) { + $this->cache->write($key, $content); + $this->cache->load($key); + } + + if (!class_exists($mainCls, false)) { + /* Last line of defense if either $this->bcWriteCacheFile was used, + * $this->cache is implemented as a no-op or we have a race condition + * where the cache was cleared between the above calls to write to and load from + * the cache. + */ + eval('?>'.$content); + } + + if (!class_exists($cls, false)) { + throw new RuntimeError(\sprintf('Failed to load Twig template "%s", index "%s": cache might be corrupted.', $name, $index), -1, $source); + } + } + } + + $this->extensionSet->initRuntime(); + + return $this->loadedTemplates[$cls] = new $cls($this); + } + + /** + * Creates a template from source. + * + * This method should not be used as a generic way to load templates. + * + * @param string $template The template source + * @param string|null $name An optional name of the template to be used in error messages + * + * @throws LoaderError When the template cannot be found + * @throws SyntaxError When an error occurred during compilation + */ + public function createTemplate(string $template, ?string $name = null): TemplateWrapper + { + $hash = hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $template, false); + if (null !== $name) { + $name = \sprintf('%s (string template %s)', $name, $hash); + } else { + $name = \sprintf('__string_template__%s', $hash); + } + + $loader = new ChainLoader([ + new ArrayLoader([$name => $template]), + $current = $this->getLoader(), + ]); + + $this->setLoader($loader); + try { + return new TemplateWrapper($this, $this->loadTemplate($this->getTemplateClass($name), $name)); + } finally { + $this->setLoader($current); + } + } + + /** + * Returns true if the template is still fresh. + * + * Besides checking the loader for freshness information, + * this method also checks if the enabled extensions have + * not changed. + * + * @param int $time The last modification time of the cached template + */ + public function isTemplateFresh(string $name, int $time): bool + { + return $this->extensionSet->getLastModified() <= $time && $this->getLoader()->isFresh($name, $time); + } + + /** + * Tries to load a template consecutively from an array. + * + * Similar to load() but it also accepts instances of \Twig\TemplateWrapper + * and an array of templates where each is tried to be loaded. + * + * @param string|TemplateWrapper|array $names A template or an array of templates to try consecutively + * + * @throws LoaderError When none of the templates can be found + * @throws SyntaxError When an error occurred during compilation + */ + public function resolveTemplate($names): TemplateWrapper + { + if (!\is_array($names)) { + return $this->load($names); + } + + $count = \count($names); + foreach ($names as $name) { + if ($name instanceof Template) { + trigger_deprecation('twig/twig', '3.9', 'Passing a "%s" instance to "%s" is deprecated.', Template::class, __METHOD__); + + return new TemplateWrapper($this, $name); + } + if ($name instanceof TemplateWrapper) { + return $name; + } + + if (1 !== $count && !$this->getLoader()->exists($name)) { + continue; + } + + return $this->load($name); + } + + throw new LoaderError(\sprintf('Unable to find one of the following templates: "%s".', implode('", "', $names))); + } + + /** + * @return void + */ + public function setLexer(Lexer $lexer) + { + $this->lexer = $lexer; + } + + /** + * @throws SyntaxError When the code is syntactically wrong + */ + public function tokenize(Source $source): TokenStream + { + if (null === $this->lexer) { + $this->lexer = new Lexer($this); + } + + return $this->lexer->tokenize($source); + } + + /** + * @return void + */ + public function setParser(Parser $parser) + { + $this->parser = $parser; + } + + /** + * Converts a token stream to a node tree. + * + * @throws SyntaxError When the token stream is syntactically or semantically wrong + */ + public function parse(TokenStream $stream): ModuleNode + { + if (null === $this->parser) { + $this->parser = new Parser($this); + } + + return $this->parser->parse($stream); + } + + /** + * @return void + */ + public function setCompiler(Compiler $compiler) + { + $this->compiler = $compiler; + } + + /** + * Compiles a node and returns the PHP code. + */ + public function compile(Node $node): string + { + if (null === $this->compiler) { + $this->compiler = new Compiler($this); + } + + return $this->compiler->compile($node)->getSource(); + } + + /** + * Compiles a template source code. + * + * @throws SyntaxError When there was an error during tokenizing, parsing or compiling + */ + public function compileSource(Source $source): string + { + try { + return $this->compile($this->parse($this->tokenize($source))); + } catch (Error $e) { + $e->setSourceContext($source); + throw $e; + } catch (\Exception $e) { + throw new SyntaxError(\sprintf('An exception has been thrown during the compilation of a template ("%s").', $e->getMessage()), -1, $source, $e); + } + } + + /** + * @return void + */ + public function setLoader(LoaderInterface $loader) + { + $this->loader = $loader; + } + + public function getLoader(): LoaderInterface + { + return $this->loader; + } + + /** + * @return void + */ + public function setCharset(string $charset) + { + if ('UTF8' === $charset = strtoupper($charset ?: '')) { + // iconv on Windows requires "UTF-8" instead of "UTF8" + $charset = 'UTF-8'; + } + + $this->charset = $charset; + } + + public function getCharset(): string + { + return $this->charset; + } + + public function hasExtension(string $class): bool + { + return $this->extensionSet->hasExtension($class); + } + + /** + * @return void + */ + public function addRuntimeLoader(RuntimeLoaderInterface $loader) + { + $this->runtimeLoaders[] = $loader; + } + + /** + * @template TExtension of ExtensionInterface + * + * @param class-string $class + * + * @return TExtension + */ + public function getExtension(string $class): ExtensionInterface + { + return $this->extensionSet->getExtension($class); + } + + /** + * Returns the runtime implementation of a Twig element (filter/function/tag/test). + * + * @template TRuntime of object + * + * @param class-string $class A runtime class name + * + * @return TRuntime The runtime implementation + * + * @throws RuntimeError When the template cannot be found + */ + public function getRuntime(string $class) + { + if (isset($this->runtimes[$class])) { + return $this->runtimes[$class]; + } + + foreach ($this->runtimeLoaders as $loader) { + if (null !== $runtime = $loader->load($class)) { + return $this->runtimes[$class] = $runtime; + } + } + + if (null !== $runtime = $this->defaultRuntimeLoader->load($class)) { + return $this->runtimes[$class] = $runtime; + } + + throw new RuntimeError(\sprintf('Unable to load the "%s" runtime.', $class)); + } + + /** + * @return void + */ + public function addExtension(ExtensionInterface $extension) + { + $this->extensionSet->addExtension($extension); + $this->updateOptionsHash(); + } + + /** + * @param ExtensionInterface[] $extensions An array of extensions + * + * @return void + */ + public function setExtensions(array $extensions) + { + $this->extensionSet->setExtensions($extensions); + $this->updateOptionsHash(); + } + + /** + * @return ExtensionInterface[] An array of extensions (keys are for internal usage only and should not be relied on) + */ + public function getExtensions(): array + { + return $this->extensionSet->getExtensions(); + } + + /** + * @return void + */ + public function addTokenParser(TokenParserInterface $parser) + { + $this->extensionSet->addTokenParser($parser); + } + + /** + * @return TokenParserInterface[] + * + * @internal + */ + public function getTokenParsers(): array + { + return $this->extensionSet->getTokenParsers(); + } + + /** + * @internal + */ + public function getTokenParser(string $name): ?TokenParserInterface + { + return $this->extensionSet->getTokenParser($name); + } + + /** + * @param callable(string): (TokenParserInterface|false) $callable + */ + public function registerUndefinedTokenParserCallback(callable $callable): void + { + $this->extensionSet->registerUndefinedTokenParserCallback($callable); + } + + /** + * @return void + */ + public function addNodeVisitor(NodeVisitorInterface $visitor) + { + $this->extensionSet->addNodeVisitor($visitor); + } + + /** + * @return NodeVisitorInterface[] + * + * @internal + */ + public function getNodeVisitors(): array + { + return $this->extensionSet->getNodeVisitors(); + } + + /** + * @return void + */ + public function addFilter(TwigFilter $filter) + { + $this->extensionSet->addFilter($filter); + } + + /** + * @internal + */ + public function getFilter(string $name): ?TwigFilter + { + return $this->extensionSet->getFilter($name); + } + + /** + * @param callable(string): (TwigFilter|false) $callable + */ + public function registerUndefinedFilterCallback(callable $callable): void + { + $this->extensionSet->registerUndefinedFilterCallback($callable); + } + + /** + * Gets the registered Filters. + * + * Be warned that this method cannot return filters defined with registerUndefinedFilterCallback. + * + * @return TwigFilter[] + * + * @see registerUndefinedFilterCallback + * + * @internal + */ + public function getFilters(): array + { + return $this->extensionSet->getFilters(); + } + + /** + * @return void + */ + public function addTest(TwigTest $test) + { + $this->extensionSet->addTest($test); + } + + /** + * @return TwigTest[] + * + * @internal + */ + public function getTests(): array + { + return $this->extensionSet->getTests(); + } + + /** + * @internal + */ + public function getTest(string $name): ?TwigTest + { + return $this->extensionSet->getTest($name); + } + + /** + * @param callable(string): (TwigTest|false) $callable + */ + public function registerUndefinedTestCallback(callable $callable): void + { + $this->extensionSet->registerUndefinedTestCallback($callable); + } + + /** + * @return void + */ + public function addFunction(TwigFunction $function) + { + $this->extensionSet->addFunction($function); + } + + /** + * @internal + */ + public function getFunction(string $name): ?TwigFunction + { + return $this->extensionSet->getFunction($name); + } + + /** + * @param callable(string): (TwigFunction|false) $callable + */ + public function registerUndefinedFunctionCallback(callable $callable): void + { + $this->extensionSet->registerUndefinedFunctionCallback($callable); + } + + /** + * Gets registered functions. + * + * Be warned that this method cannot return functions defined with registerUndefinedFunctionCallback. + * + * @return TwigFunction[] + * + * @see registerUndefinedFunctionCallback + * + * @internal + */ + public function getFunctions(): array + { + return $this->extensionSet->getFunctions(); + } + + /** + * Registers a Global. + * + * New globals can be added before compiling or rendering a template; + * but after, you can only update existing globals. + * + * @param mixed $value The global value + * + * @return void + */ + public function addGlobal(string $name, $value) + { + if ($this->extensionSet->isInitialized() && !\array_key_exists($name, $this->getGlobals())) { + throw new \LogicException(\sprintf('Unable to add global "%s" as the runtime or the extensions have already been initialized.', $name)); + } + + if (null !== $this->resolvedGlobals) { + $this->resolvedGlobals[$name] = $value; + } else { + $this->globals[$name] = $value; + } + } + + /** + * @return array + */ + public function getGlobals(): array + { + if ($this->extensionSet->isInitialized()) { + if (null === $this->resolvedGlobals) { + $this->resolvedGlobals = array_merge($this->extensionSet->getGlobals(), $this->globals); + } + + return $this->resolvedGlobals; + } + + return array_merge($this->extensionSet->getGlobals(), $this->globals); + } + + public function resetGlobals(): void + { + $this->resolvedGlobals = null; + $this->extensionSet->resetGlobals(); + } + + /** + * @deprecated since Twig 3.14 + */ + public function mergeGlobals(array $context): array + { + trigger_deprecation('twig/twig', '3.14', 'The "%s" method is deprecated.', __METHOD__); + + return $context + $this->getGlobals(); + } + + /** + * @internal + */ + public function getExpressionParsers(): ExpressionParsers + { + return $this->extensionSet->getExpressionParsers(); + } + + private function updateOptionsHash(): void + { + $this->optionsHash = implode(':', [ + $this->extensionSet->getSignature(), + \PHP_MAJOR_VERSION, + \PHP_MINOR_VERSION, + self::VERSION, + (int) $this->debug, + (int) $this->strictVariables, + $this->useYield ? '1' : '0', + ]); + } +} diff --git a/novaconium/vendor/twig/src/Error/Error.php b/novaconium/vendor/twig/src/Error/Error.php new file mode 100644 index 0000000..8494d24 --- /dev/null +++ b/novaconium/vendor/twig/src/Error/Error.php @@ -0,0 +1,208 @@ + + */ +class Error extends \Exception +{ + private $lineno; + /** @var positive-int|null */ + private ?int $columnno; + private $rawMessage; + private ?Source $source; + private string $phpFile; + private int $phpLine; + + /** + * Constructor. + * + * By default, automatic guessing is enabled. + * + * @param string $message The error message + * @param int $lineno The template line where the error occurred + * @param Source|null $source The source context where the error occurred + * @param positive-int|null $columnno The template column where the error occurred + */ + public function __construct(string $message, int $lineno = -1, ?Source $source = null, ?\Throwable $previous = null, ?int $columnno = null) + { + parent::__construct('', 0, $previous); + + $this->phpFile = $this->getFile(); + $this->phpLine = $this->getLine(); + $this->lineno = $lineno; + $this->columnno = $columnno; + $this->source = $source; + $this->rawMessage = $message; + $this->updateRepr(); + } + + public function getRawMessage(): string + { + return $this->rawMessage; + } + + public function getTemplateLine(): int + { + return $this->lineno; + } + + public function setTemplateLine(int $lineno): void + { + $this->lineno = $lineno; + $this->updateRepr(); + } + + /** + * Returns the 1-based column where the error occurred, or null if unknown. + * + * @return positive-int|null + */ + public function getTemplateColumn(): ?int + { + return $this->columnno; + } + + /** + * @param positive-int|null $columnno + */ + public function setTemplateColumn(?int $columnno): void + { + $this->columnno = $columnno; + $this->updateRepr(); + } + + public function getSourceContext(): ?Source + { + return $this->source; + } + + public function setSourceContext(?Source $source = null): void + { + $this->source = $source; + $this->updateRepr(); + } + + public function guess(): void + { + if ($this->lineno > -1) { + return; + } + + $this->guessTemplateInfo(); + $this->updateRepr(); + } + + public function appendMessage($rawMessage): void + { + $this->rawMessage .= $rawMessage; + $this->updateRepr(); + } + + private function updateRepr(): void + { + if ($this->source && $this->source->getPath()) { + // we only update the file and the line together + $this->file = $this->source->getPath(); + if ($this->lineno > 0) { + $this->line = $this->lineno; + } else { + $this->line = -1; + } + } + + $this->message = $this->rawMessage; + $last = substr($this->message, -1); + if ($punctuation = '.' === $last || '?' === $last ? $last : '') { + $this->message = substr($this->message, 0, -1); + } + if ($this->source && $this->source->getName()) { + $this->message .= \sprintf(' in "%s"', $this->source->getName()); + } + if ($this->lineno > 0) { + $this->message .= \sprintf(' at line %d', $this->lineno); + if (null !== $this->columnno) { + $this->message .= \sprintf(' column %d', $this->columnno); + } + } + if ($punctuation) { + $this->message .= $punctuation; + } + } + + private function guessTemplateInfo(): void + { + // $this->source is never null here (see guess() usage in Template) + + $this->lineno = 0; + $template = null; + $backtrace = debug_backtrace(\DEBUG_BACKTRACE_IGNORE_ARGS | \DEBUG_BACKTRACE_PROVIDE_OBJECT); + foreach ($backtrace as $trace) { + if (isset($trace['object']) && $trace['object'] instanceof Template && $this->source->getName() === $trace['object']->getTemplateName()) { + $template = $trace['object']; + + break; + } + } + + if (null === $template) { + return; // Impossible to guess the info as the template was not found in the backtrace + } + + $r = new \ReflectionObject($template); + $file = $r->getFileName(); + + $exceptions = [$e = $this]; + while ($e = $e->getPrevious()) { + $exceptions[] = $e; + } + + while ($e = array_pop($exceptions)) { + $traces = $e->getTrace(); + array_unshift($traces, ['file' => $e instanceof self ? $e->phpFile : $e->getFile(), 'line' => $e instanceof self ? $e->phpLine : $e->getLine()]); + while ($trace = array_shift($traces)) { + if (!isset($trace['file']) || !isset($trace['line']) || $file != $trace['file']) { + continue; + } + + foreach ($template->getDebugInfo() as $codeLine => $templateLine) { + if ($codeLine <= $trace['line']) { + // update template line + $this->lineno = $templateLine; + + return; + } + } + } + } + } +} diff --git a/novaconium/vendor/twig/src/Error/LoaderError.php b/novaconium/vendor/twig/src/Error/LoaderError.php new file mode 100644 index 0000000..7c8c23c --- /dev/null +++ b/novaconium/vendor/twig/src/Error/LoaderError.php @@ -0,0 +1,21 @@ + + */ +class LoaderError extends Error +{ +} diff --git a/novaconium/vendor/twig/src/Error/RuntimeError.php b/novaconium/vendor/twig/src/Error/RuntimeError.php new file mode 100644 index 0000000..f6b8476 --- /dev/null +++ b/novaconium/vendor/twig/src/Error/RuntimeError.php @@ -0,0 +1,22 @@ + + */ +class RuntimeError extends Error +{ +} diff --git a/novaconium/vendor/twig/src/Error/SyntaxError.php b/novaconium/vendor/twig/src/Error/SyntaxError.php new file mode 100644 index 0000000..841b653 --- /dev/null +++ b/novaconium/vendor/twig/src/Error/SyntaxError.php @@ -0,0 +1,46 @@ + + */ +class SyntaxError extends Error +{ + /** + * Tweaks the error message to include suggestions. + * + * @param string $name The original name of the item that does not exist + * @param array $items An array of possible items + */ + public function addSuggestions(string $name, array $items): void + { + $alternatives = []; + foreach ($items as $item) { + $lev = levenshtein($name, $item); + if ($lev <= \strlen($name) / 3 || str_contains($item, $name)) { + $alternatives[$item] = $lev; + } + } + + if (!$alternatives) { + return; + } + + asort($alternatives); + + $this->appendMessage(\sprintf(' Did you mean "%s"?', implode('", "', array_keys($alternatives)))); + } +} diff --git a/novaconium/vendor/twig/src/ExpressionParser.php b/novaconium/vendor/twig/src/ExpressionParser.php new file mode 100644 index 0000000..3ba94d0 --- /dev/null +++ b/novaconium/vendor/twig/src/ExpressionParser.php @@ -0,0 +1,369 @@ + + * + * @deprecated since Twig 3.21 + */ +class ExpressionParser +{ + /** + * @deprecated since Twig 3.21 + */ + public const OPERATOR_LEFT = 1; + /** + * @deprecated since Twig 3.21 + */ + public const OPERATOR_RIGHT = 2; + + public function __construct( + private Parser $parser, + private Environment $env, + ) { + trigger_deprecation('twig/twig', '3.21', 'Class "%s" is deprecated, use "Parser::parseExpression()" instead.', __CLASS__); + } + + public function parseExpression($precedence = 0) + { + if (\func_num_args() > 1) { + trigger_deprecation('twig/twig', '3.15', 'Passing a second argument ($allowArrow) to "%s()" is deprecated.', __METHOD__); + } + + trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated, use "Parser::parseExpression()" instead.', __METHOD__); + + return $this->parser->parseExpression((int) $precedence); + } + + /** + * @deprecated since Twig 3.21 + */ + public function parsePrimaryExpression() + { + trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__); + + return $this->parseExpression(); + } + + /** + * @deprecated since Twig 3.21 + */ + public function parseStringExpression() + { + trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__); + + return $this->parseExpression(); + } + + /** + * @deprecated since Twig 3.11, use parseExpression() instead + */ + public function parseArrayExpression() + { + trigger_deprecation('twig/twig', '3.11', 'Calling "%s()" is deprecated, use "parseExpression()" instead.', __METHOD__); + + return $this->parseExpression(); + } + + /** + * @deprecated since Twig 3.21 + */ + public function parseSequenceExpression() + { + trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__); + + return $this->parseExpression(); + } + + /** + * @deprecated since Twig 3.11, use parseExpression() instead + */ + public function parseHashExpression() + { + trigger_deprecation('twig/twig', '3.11', 'Calling "%s()" is deprecated, use "parseExpression()" instead.', __METHOD__); + + return $this->parseExpression(); + } + + /** + * @deprecated since Twig 3.21 + */ + public function parseMappingExpression() + { + trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__); + + return $this->parseExpression(); + } + + /** + * @deprecated since Twig 3.21 + */ + public function parsePostfixExpression($node) + { + trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__); + + while (true) { + $token = $this->parser->getCurrentToken(); + if ($token->test(Token::PUNCTUATION_TYPE)) { + if ('.' == $token->getValue() || '[' == $token->getValue()) { + $node = $this->parseSubscriptExpression($node); + } elseif ('|' == $token->getValue()) { + $node = $this->parseFilterExpression($node); + } else { + break; + } + } else { + break; + } + } + + return $node; + } + + /** + * @deprecated since Twig 3.21 + */ + public function parseSubscriptExpression($node) + { + trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__); + + $parsers = new \ReflectionProperty($this->parser, 'parsers'); + + if ('.' === $this->parser->getStream()->next()->getValue()) { + return $parsers->getValue($this->parser)->getByClass(DotExpressionParser::class)->parse($this->parser, $node, $this->parser->getCurrentToken()); + } + + return $parsers->getValue($this->parser)->getByClass(SquareBracketExpressionParser::class)->parse($this->parser, $node, $this->parser->getCurrentToken()); + } + + /** + * @deprecated since Twig 3.21 + */ + public function parseFilterExpression($node) + { + trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__); + + $this->parser->getStream()->next(); + + return $this->parseFilterExpressionRaw($node); + } + + /** + * @deprecated since Twig 3.21 + */ + public function parseFilterExpressionRaw($node) + { + trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__); + + $parsers = new \ReflectionProperty($this->parser, 'parsers'); + + $op = $parsers->getValue($this->parser)->getByClass(FilterExpressionParser::class); + while (true) { + $node = $op->parse($this->parser, $node, $this->parser->getCurrentToken()); + if (!$this->parser->getStream()->test(Token::OPERATOR_TYPE, '|')) { + break; + } + $this->parser->getStream()->next(); + } + + return $node; + } + + /** + * Parses arguments. + * + * @return Node + * + * @throws SyntaxError + * + * @deprecated since Twig 3.19 Use Twig\ExpressionParser\Infix\ArgumentsTrait::parseNamedArguments() instead + */ + public function parseArguments() + { + trigger_deprecation('twig/twig', '3.19', \sprintf('The "%s()" method is deprecated, use "Twig\ExpressionParser\Infix\ArgumentsTrait::parseNamedArguments()" instead.', __METHOD__)); + + $parsePrimaryExpression = new \ReflectionMethod($this->parser, 'parsePrimaryExpression'); + + $namedArguments = false; + $definition = false; + if (\func_num_args() > 1) { + $definition = func_get_arg(1); + } + if (\func_num_args() > 0) { + trigger_deprecation('twig/twig', '3.15', 'Passing arguments to "%s()" is deprecated.', __METHOD__); + $namedArguments = func_get_arg(0); + } + + $args = []; + $stream = $this->parser->getStream(); + + $stream->expect(Token::OPERATOR_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); + $hasSpread = false; + while (!$stream->test(Token::PUNCTUATION_TYPE, ')')) { + if ($args) { + $stream->expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma'); + + // if the comma above was a trailing comma, early exit the argument parse loop + if ($stream->test(Token::PUNCTUATION_TYPE, ')')) { + break; + } + } + + if ($definition) { + $token = $stream->expect(Token::NAME_TYPE, null, 'An argument must be a name'); + $value = new ContextVariable($token->getValue(), $this->parser->getCurrentToken()->getLine()); + } else { + if ($stream->nextIf(Token::SPREAD_TYPE)) { + $hasSpread = true; + $value = new SpreadUnary($this->parseExpression(), $stream->getCurrent()->getLine()); + } elseif ($hasSpread) { + throw new SyntaxError('Normal arguments must be placed before argument unpacking.', $stream->getCurrent()->getLine(), $stream->getSourceContext()); + } else { + $value = $this->parseExpression(); + } + } + + $name = null; + if ($namedArguments && (($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) || (!$definition && $token = $stream->nextIf(Token::PUNCTUATION_TYPE, ':')))) { + if (!$value instanceof ContextVariable) { + throw new SyntaxError(\sprintf('A parameter name must be a string, "%s" given.', $value::class), $token->getLine(), $stream->getSourceContext()); + } + $name = $value->getAttribute('name'); + + if ($definition) { + $value = $parsePrimaryExpression->invoke($this->parser); + + if (!$this->checkConstantExpression($value)) { + throw new SyntaxError('A default value for an argument must be a constant (a boolean, a string, a number, a sequence, or a mapping).', $token->getLine(), $stream->getSourceContext()); + } + } else { + $value = $this->parseExpression(); + } + } + + if ($definition) { + if (null === $name) { + $name = $value->getAttribute('name'); + $value = new ConstantExpression(null, $this->parser->getCurrentToken()->getLine()); + $value->setAttribute('is_implicit', true); + } + $args[$name] = $value; + } else { + if (null === $name) { + $args[] = $value; + } else { + $args[$name] = $value; + } + } + } + $stream->expect(Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis'); + + return new Nodes($args); + } + + /** + * @deprecated since Twig 3.21, use "AbstractTokenParser::parseAssignmentExpression()" instead + */ + public function parseAssignmentExpression() + { + trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated, use "AbstractTokenParser::parseAssignmentExpression()" instead.', __METHOD__); + + $stream = $this->parser->getStream(); + $targets = []; + while (true) { + $token = $this->parser->getCurrentToken(); + if ($stream->test(Token::OPERATOR_TYPE) && preg_match(Lexer::REGEX_NAME, $token->getValue())) { + // in this context, string operators are variable names + $this->parser->getStream()->next(); + } else { + $stream->expect(Token::NAME_TYPE, null, 'Only variables can be assigned to'); + } + $targets[] = new AssignContextVariable($token->getValue(), $token->getLine()); + + if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) { + break; + } + } + + return new Nodes($targets); + } + + /** + * @deprecated since Twig 3.21 + */ + public function parseMultitargetExpression() + { + trigger_deprecation('twig/twig', '3.21', 'The "%s()" method is deprecated.', __METHOD__); + + $targets = []; + while (true) { + $targets[] = $this->parseExpression(); + if (!$this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ',')) { + break; + } + } + + return new Nodes($targets); + } + + // checks that the node only contains "constant" elements + // to be removed in 4.0 + private function checkConstantExpression(Node $node): bool + { + if (!($node instanceof ConstantExpression || $node instanceof ArrayExpression + || $node instanceof NegUnary || $node instanceof PosUnary + )) { + return false; + } + + foreach ($node as $n) { + if (!$this->checkConstantExpression($n)) { + return false; + } + } + + return true; + } + + /** + * @deprecated since Twig 3.19 Use Twig\ExpressionParser\Infix\ArgumentsTrait::parseNamedArguments() instead + */ + public function parseOnlyArguments() + { + trigger_deprecation('twig/twig', '3.19', \sprintf('The "%s()" method is deprecated, use "Twig\ExpressionParser\Infix\ArgumentsTrait::parseNamedArguments()" instead.', __METHOD__)); + + return $this->parseArguments(); + } +} diff --git a/novaconium/vendor/twig/src/ExpressionParser/AbstractExpressionParser.php b/novaconium/vendor/twig/src/ExpressionParser/AbstractExpressionParser.php new file mode 100644 index 0000000..b8b963a --- /dev/null +++ b/novaconium/vendor/twig/src/ExpressionParser/AbstractExpressionParser.php @@ -0,0 +1,35 @@ +value, $this->getName()); + } + + public function getPrecedenceChange(): ?PrecedenceChange + { + return null; + } + + public function getAliases(): array + { + return []; + } + + public function getOperatorTokens(): array + { + return [$this->getName(), ...$this->getAliases()]; + } +} diff --git a/novaconium/vendor/twig/src/ExpressionParser/ExpressionParserDescriptionInterface.php b/novaconium/vendor/twig/src/ExpressionParser/ExpressionParserDescriptionInterface.php new file mode 100644 index 0000000..686f8a5 --- /dev/null +++ b/novaconium/vendor/twig/src/ExpressionParser/ExpressionParserDescriptionInterface.php @@ -0,0 +1,17 @@ + getOperatorTokens() Returns the operator token strings that this expression parser handles. + * These are the strings that should be recognized as operator tokens by the Lexer, + * and used to look up the parser in the registry. + * For most parsers, this returns the name and aliases. Parsers that don't handle + * operator tokens (like LiteralExpressionParser) should return an empty array. + * This method will be added to the interface in Twig 4.0. + */ +interface ExpressionParserInterface +{ + public function __toString(): string; + + public function getName(): string; + + public function getPrecedence(): int; + + public function getPrecedenceChange(): ?PrecedenceChange; + + /** + * @return array + */ + public function getAliases(): array; +} diff --git a/novaconium/vendor/twig/src/ExpressionParser/ExpressionParserType.php b/novaconium/vendor/twig/src/ExpressionParser/ExpressionParserType.php new file mode 100644 index 0000000..8c21a8d --- /dev/null +++ b/novaconium/vendor/twig/src/ExpressionParser/ExpressionParserType.php @@ -0,0 +1,33 @@ + + * + * @internal + */ +final class ExpressionParsers implements \IteratorAggregate +{ + /** + * @var array, array> + */ + private array $parsersByName = []; + + /** + * @var array, ExpressionParserInterface> + */ + private array $parsersByClass = []; + + /** + * @var \WeakMap>|null + */ + private ?\WeakMap $precedenceChanges = null; + + /** + * @param array $parsers + */ + public function __construct(array $parsers = []) + { + $this->add($parsers); + } + + /** + * @param array $parsers + * + * @return $this + */ + public function add(array $parsers): static + { + foreach ($parsers as $parser) { + if ($parser->getPrecedence() > 512 || $parser->getPrecedence() < 0) { + trigger_deprecation('twig/twig', '3.21', 'Precedence for "%s" must be between 0 and 512, got %d.', $parser->getName(), $parser->getPrecedence()); + // throw new \InvalidArgumentException(\sprintf('Precedence for "%s" must be between 0 and 512, got %d.', $parser->getName(), $parser->getPrecedence())); + } + $interface = $parser instanceof PrefixExpressionParserInterface ? PrefixExpressionParserInterface::class : InfixExpressionParserInterface::class; + $this->parsersByClass[$parser::class] = $parser; + foreach (self::getOperatorTokensFor($parser) as $token) { + $this->parsersByName[$interface][$token] = $parser; + } + } + + return $this; + } + + /** + * @template T of ExpressionParserInterface + * + * @param class-string $class + * + * @return T|null + */ + public function getByClass(string $class): ?ExpressionParserInterface + { + return $this->parsersByClass[$class] ?? null; + } + + /** + * @template T of ExpressionParserInterface + * + * @param class-string $interface + * + * @return T|null + */ + public function getByName(string $interface, string $name): ?ExpressionParserInterface + { + return $this->parsersByName[$interface][$name] ?? null; + } + + public function getIterator(): \Traversable + { + $seen = []; + foreach ($this->parsersByName as $parsers) { + foreach ($parsers as $parser) { + $id = spl_object_id($parser); + if (!isset($seen[$id])) { + $seen[$id] = true; + yield $parser; + } + } + } + foreach ($this->parsersByClass as $parser) { + $id = spl_object_id($parser); + if (!isset($seen[$id])) { + $seen[$id] = true; + yield $parser; + } + } + } + + /** + * @internal + * + * @return \WeakMap> + */ + public function getPrecedenceChanges(): \WeakMap + { + if (null === $this->precedenceChanges) { + $this->precedenceChanges = new \WeakMap(); + foreach ($this as $ep) { + if (!$ep->getPrecedenceChange()) { + continue; + } + $min = min($ep->getPrecedenceChange()->getNewPrecedence(), $ep->getPrecedence()); + $max = max($ep->getPrecedenceChange()->getNewPrecedence(), $ep->getPrecedence()); + foreach ($this as $e) { + if ($e->getPrecedence() > $min && $e->getPrecedence() < $max) { + if (!isset($this->precedenceChanges[$e])) { + $this->precedenceChanges[$e] = []; + } + $this->precedenceChanges[$e][] = $ep; + } + } + } + } + + return $this->precedenceChanges; + } + + /** + * @internal + * + * @return array + */ + public static function getOperatorTokensFor(ExpressionParserInterface $parser): array + { + if (method_exists($parser, 'getOperatorTokens')) { + return $parser->getOperatorTokens(); + } + + trigger_deprecation('twig/twig', '3.24', 'Not implementing the "getOperatorTokens()" method in "%s" is deprecated. This method will be part of the "%s" interface in 4.0.', $parser::class, ExpressionParserInterface::class); + + return [$parser->getName(), ...$parser->getAliases()]; + } +} diff --git a/novaconium/vendor/twig/src/ExpressionParser/Infix/ArgumentsTrait.php b/novaconium/vendor/twig/src/ExpressionParser/Infix/ArgumentsTrait.php new file mode 100644 index 0000000..96bde55 --- /dev/null +++ b/novaconium/vendor/twig/src/ExpressionParser/Infix/ArgumentsTrait.php @@ -0,0 +1,83 @@ +parseNamedArguments($parser, $parseOpenParenthesis) as $k => $n) { + $arguments->addElement($n, new LocalVariable($k, $line)); + } + + return $arguments; + } + + private function parseNamedArguments(Parser $parser, bool $parseOpenParenthesis = true): Nodes + { + $args = []; + $stream = $parser->getStream(); + if ($parseOpenParenthesis) { + $stream->expect(Token::OPERATOR_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); + } + $hasSpread = false; + while (!$stream->test(Token::PUNCTUATION_TYPE, ')')) { + if ($args) { + $stream->expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma'); + + // if the comma above was a trailing comma, early exit the argument parse loop + if ($stream->test(Token::PUNCTUATION_TYPE, ')')) { + break; + } + } + + $value = $parser->parseExpression(); + if ($value instanceof SpreadUnary) { + $hasSpread = true; + } elseif ($hasSpread) { + throw new SyntaxError('Normal arguments must be placed before argument unpacking.', $stream->getCurrent()->getLine(), $stream->getSourceContext()); + } + + $name = null; + if ($value instanceof SetBinary) { + $name = $value->getNode('left')->getAttribute('name'); + $value = $value->getNode('right'); + } elseif (($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) || ($token = $stream->nextIf(Token::PUNCTUATION_TYPE, ':'))) { + if (!$value instanceof ContextVariable) { + throw new SyntaxError(\sprintf('A parameter name must be a string, "%s" given.', $value::class), $token->getLine(), $stream->getSourceContext()); + } + $name = $value->getAttribute('name'); + $value = $parser->parseExpression(); + } + + if (null === $name) { + $args[] = $value; + } else { + $args[$name] = $value; + } + } + $stream->expect(Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis'); + + return new Nodes($args); + } +} diff --git a/novaconium/vendor/twig/src/ExpressionParser/Infix/ArrowExpressionParser.php b/novaconium/vendor/twig/src/ExpressionParser/Infix/ArrowExpressionParser.php new file mode 100644 index 0000000..c8630da --- /dev/null +++ b/novaconium/vendor/twig/src/ExpressionParser/Infix/ArrowExpressionParser.php @@ -0,0 +1,53 @@ +parseExpression(), $expr, $token->getLine()); + } + + public function getName(): string + { + return '=>'; + } + + public function getDescription(): string + { + return 'Arrow function (x => expr)'; + } + + public function getPrecedence(): int + { + return 250; + } + + public function getAssociativity(): InfixAssociativity + { + return InfixAssociativity::Left; + } +} diff --git a/novaconium/vendor/twig/src/ExpressionParser/Infix/AssignmentExpressionParser.php b/novaconium/vendor/twig/src/ExpressionParser/Infix/AssignmentExpressionParser.php new file mode 100644 index 0000000..1f04284 --- /dev/null +++ b/novaconium/vendor/twig/src/ExpressionParser/Infix/AssignmentExpressionParser.php @@ -0,0 +1,66 @@ +getLine(), $parser->getStream()->getSourceContext()); + } + $right = $parser->parseExpression(InfixAssociativity::Left === $this->getAssociativity() ? $this->getPrecedence() + 1 : $this->getPrecedence()); + $right = match ($this->getName()) { + '=' => $right, + default => throw new \LogicException(\sprintf('Unknown operator: %s.', $this->getName())), + }; + + if ($left instanceof ArrayExpression) { + if ($left->isSequence()) { + return new SequenceDestructuringSetBinary($left, $right, $token->getLine()); + } + + return new ObjectDestructuringSetBinary($left, $right, $token->getLine()); + } + + return new SetBinary($left, $right, $token->getLine()); + } + + public function getDescription(): string + { + return 'Assignment operator'; + } +} diff --git a/novaconium/vendor/twig/src/ExpressionParser/Infix/BinaryOperatorExpressionParser.php b/novaconium/vendor/twig/src/ExpressionParser/Infix/BinaryOperatorExpressionParser.php new file mode 100644 index 0000000..4c66da7 --- /dev/null +++ b/novaconium/vendor/twig/src/ExpressionParser/Infix/BinaryOperatorExpressionParser.php @@ -0,0 +1,80 @@ + */ + private string $nodeClass, + private string $name, + private int $precedence, + private InfixAssociativity $associativity = InfixAssociativity::Left, + private ?PrecedenceChange $precedenceChange = null, + private ?string $description = null, + private array $aliases = [], + ) { + } + + /** + * @return AbstractBinary + */ + public function parse(Parser $parser, AbstractExpression $left, Token $token): AbstractExpression + { + $right = $parser->parseExpression(InfixAssociativity::Left === $this->getAssociativity() ? $this->getPrecedence() + 1 : $this->getPrecedence()); + + return new ($this->nodeClass)($left, $right, $token->getLine()); + } + + public function getAssociativity(): InfixAssociativity + { + return $this->associativity; + } + + public function getName(): string + { + return $this->name; + } + + public function getDescription(): string + { + return $this->description ?? ''; + } + + public function getPrecedence(): int + { + return $this->precedence; + } + + public function getPrecedenceChange(): ?PrecedenceChange + { + return $this->precedenceChange; + } + + public function getAliases(): array + { + return $this->aliases; + } +} diff --git a/novaconium/vendor/twig/src/ExpressionParser/Infix/ConditionalTernaryExpressionParser.php b/novaconium/vendor/twig/src/ExpressionParser/Infix/ConditionalTernaryExpressionParser.php new file mode 100644 index 0000000..9707c0a --- /dev/null +++ b/novaconium/vendor/twig/src/ExpressionParser/Infix/ConditionalTernaryExpressionParser.php @@ -0,0 +1,62 @@ +parseExpression($this->getPrecedence()); + if ($parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ':')) { + // Ternary operator (expr ? expr2 : expr3) + $else = $parser->parseExpression($this->getPrecedence()); + } else { + // Ternary without else (expr ? expr2) + $else = new ConstantExpression('', $token->getLine()); + } + + return new ConditionalTernary($left, $then, $else, $token->getLine()); + } + + public function getName(): string + { + return '?'; + } + + public function getDescription(): string + { + return 'Conditional operator (a ? b : c)'; + } + + public function getPrecedence(): int + { + return 0; + } + + public function getAssociativity(): InfixAssociativity + { + return InfixAssociativity::Left; + } +} diff --git a/novaconium/vendor/twig/src/ExpressionParser/Infix/DotExpressionParser.php b/novaconium/vendor/twig/src/ExpressionParser/Infix/DotExpressionParser.php new file mode 100644 index 0000000..287fbd1 --- /dev/null +++ b/novaconium/vendor/twig/src/ExpressionParser/Infix/DotExpressionParser.php @@ -0,0 +1,114 @@ +getValue(); + $stream = $parser->getStream(); + $token = $stream->getCurrent(); + $lineno = $token->getLine(); + $arguments = new ArrayExpression([], $lineno); + $type = Template::ANY_CALL; + + if ($stream->nextIf(Token::OPERATOR_TYPE, '(')) { + $attribute = $parser->parseExpression(); + $stream->expect(Token::PUNCTUATION_TYPE, ')'); + } else { + $token = $stream->next(); + if ( + $token->test(Token::NAME_TYPE) + || $token->test(Token::NUMBER_TYPE) + || ($token->test(Token::OPERATOR_TYPE) && preg_match(Lexer::REGEX_NAME, $token->getValue())) + ) { + $attribute = new ConstantExpression($token->getValue(), $token->getLine()); + } else { + throw new SyntaxError(\sprintf('Expected name or number, got value "%s" of type "%s".', $token->getValue(), $token->toEnglish()), $token->getLine(), $stream->getSourceContext()); + } + } + + if ($stream->test(Token::OPERATOR_TYPE, '(')) { + $type = Template::METHOD_CALL; + $arguments = $this->parseCallableArguments($parser, $token->getLine()); + } + + $isMacroTarget = $expr instanceof NameExpression + && ( + null !== $parser->getImportedSymbol('template', $expr->getAttribute('name')) + || '_self' === $expr->getAttribute('name') + ); + + if ( + $isMacroTarget + && $attribute instanceof ConstantExpression + && \is_string($name = $attribute->getAttribute('value')) + && preg_match('#^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$#D', $name) + ) { + return new MacroReferenceExpression(new TemplateVariable($expr->getAttribute('name'), $expr->getTemplateLine()), 'macro_'.$name, $arguments, $expr->getTemplateLine()); + } + + if ($isMacroTarget && !$attribute instanceof ConstantExpression) { + return new MacroReferenceExpression(new TemplateVariable($expr->getAttribute('name'), $expr->getTemplateLine()), $attribute, $arguments, $expr->getTemplateLine()); + } + + return new GetAttrExpression($expr, $attribute, $arguments, $type, $lineno, $nullSafe); + } + + public function getName(): string + { + return '.'; + } + + public function getAliases(): array + { + return ['?.']; + } + + public function getDescription(): string + { + return 'Get an attribute on a variable'; + } + + public function getPrecedence(): int + { + return 512; + } + + public function getAssociativity(): InfixAssociativity + { + return InfixAssociativity::Left; + } +} diff --git a/novaconium/vendor/twig/src/ExpressionParser/Infix/FilterExpressionParser.php b/novaconium/vendor/twig/src/ExpressionParser/Infix/FilterExpressionParser.php new file mode 100644 index 0000000..0bbe6b4 --- /dev/null +++ b/novaconium/vendor/twig/src/ExpressionParser/Infix/FilterExpressionParser.php @@ -0,0 +1,85 @@ +getStream(); + $token = $stream->expect(Token::NAME_TYPE); + $line = $token->getLine(); + + if (!$stream->test(Token::OPERATOR_TYPE, '(')) { + $arguments = new EmptyNode(); + } else { + $arguments = $this->parseNamedArguments($parser); + } + + $filter = $parser->getFilter($token->getValue(), $line); + + $ready = true; + if (!isset($this->readyNodes[$class = $filter->getNodeClass()])) { + $this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class); + } + + if (!$ready = $this->readyNodes[$class]) { + trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigFilter" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class); + } + + return new $class($expr, $ready ? $filter : new ConstantExpression($filter->getName(), $line), $arguments, $line); + } + + public function getName(): string + { + return '|'; + } + + public function getDescription(): string + { + return 'Twig filter call'; + } + + public function getPrecedence(): int + { + return 512; + } + + public function getPrecedenceChange(): ?PrecedenceChange + { + return new PrecedenceChange('twig/twig', '3.21', 300); + } + + public function getAssociativity(): InfixAssociativity + { + return InfixAssociativity::Left; + } +} diff --git a/novaconium/vendor/twig/src/ExpressionParser/Infix/FunctionExpressionParser.php b/novaconium/vendor/twig/src/ExpressionParser/Infix/FunctionExpressionParser.php new file mode 100644 index 0000000..c885182 --- /dev/null +++ b/novaconium/vendor/twig/src/ExpressionParser/Infix/FunctionExpressionParser.php @@ -0,0 +1,97 @@ +getLine(); + if (!$expr instanceof NameExpression) { + throw new SyntaxError('Function name must be an identifier.', $line, $parser->getStream()->getSourceContext()); + } + + $name = $expr->getAttribute('name'); + + if (null !== $alias = $parser->getImportedSymbol('function', $name)) { + return new MacroReferenceExpression($alias['node']->getNode('var'), $alias['name'], $this->parseCallableArguments($parser, $line, false), $line); + } + + $args = $this->parseNamedArguments($parser, false); + + $function = $parser->getFunction($name, $line); + + if ($function->getParserCallable()) { + $fakeNode = new EmptyNode($line); + $fakeNode->setSourceContext($parser->getStream()->getSourceContext()); + + $node = ($function->getParserCallable())($parser, $fakeNode, $args, $line); + // remember the original function name so the sandbox can enforce + // the `allowedFunctions` allow-list even though the parser callable + // returned a specialized node (e.g. `parent`, `block`, `attribute`). + $node->setAttribute('sandboxed_function_name', $name); + $node->setAttribute('sandboxed_function', $function); + + return $node; + } + + if (!isset($this->readyNodes[$class = $function->getNodeClass()])) { + $this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class); + } + + if (!$ready = $this->readyNodes[$class]) { + trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigFunction" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class); + } + + return new $class($ready ? $function : $function->getName(), $args, $line); + } + + public function getName(): string + { + return '('; + } + + public function getDescription(): string + { + return 'Twig function call'; + } + + public function getPrecedence(): int + { + return 512; + } + + public function getAssociativity(): InfixAssociativity + { + return InfixAssociativity::Left; + } +} diff --git a/novaconium/vendor/twig/src/ExpressionParser/Infix/IsExpressionParser.php b/novaconium/vendor/twig/src/ExpressionParser/Infix/IsExpressionParser.php new file mode 100644 index 0000000..88d54f7 --- /dev/null +++ b/novaconium/vendor/twig/src/ExpressionParser/Infix/IsExpressionParser.php @@ -0,0 +1,84 @@ +getStream(); + $test = $parser->getTest($token->getLine()); + + $arguments = null; + if ($stream->test(Token::OPERATOR_TYPE, '(')) { + $arguments = $this->parseNamedArguments($parser); + } elseif ($test->hasOneMandatoryArgument()) { + $arguments = new Nodes([0 => $parser->parseExpression($this->getPrecedence())]); + } + + if ('defined' === $test->getName() && $expr instanceof NameExpression && null !== $alias = $parser->getImportedSymbol('function', $expr->getAttribute('name'))) { + $expr = new MacroReferenceExpression($alias['node']->getNode('var'), $alias['name'], new ArrayExpression([], $expr->getTemplateLine()), $expr->getTemplateLine()); + } + + $ready = $test instanceof TwigTest; + if (!isset($this->readyNodes[$class = $test->getNodeClass()])) { + $this->readyNodes[$class] = (bool) (new \ReflectionClass($class))->getConstructor()->getAttributes(FirstClassTwigCallableReady::class); + } + + if (!$ready = $this->readyNodes[$class]) { + trigger_deprecation('twig/twig', '3.12', 'Twig node "%s" is not marked as ready for passing a "TwigTest" in the constructor instead of its name; please update your code and then add #[FirstClassTwigCallableReady] attribute to the constructor.', $class); + } + + return new $class($expr, $ready ? $test : $test->getName(), $arguments, $stream->getCurrent()->getLine()); + } + + public function getPrecedence(): int + { + return 100; + } + + public function getName(): string + { + return 'is'; + } + + public function getDescription(): string + { + return 'Twig tests'; + } + + public function getAssociativity(): InfixAssociativity + { + return InfixAssociativity::Left; + } +} diff --git a/novaconium/vendor/twig/src/ExpressionParser/Infix/IsNotExpressionParser.php b/novaconium/vendor/twig/src/ExpressionParser/Infix/IsNotExpressionParser.php new file mode 100644 index 0000000..1e1085a --- /dev/null +++ b/novaconium/vendor/twig/src/ExpressionParser/Infix/IsNotExpressionParser.php @@ -0,0 +1,33 @@ +getLine()); + } + + public function getName(): string + { + return 'is not'; + } +} diff --git a/novaconium/vendor/twig/src/ExpressionParser/Infix/SquareBracketExpressionParser.php b/novaconium/vendor/twig/src/ExpressionParser/Infix/SquareBracketExpressionParser.php new file mode 100644 index 0000000..c47c91d --- /dev/null +++ b/novaconium/vendor/twig/src/ExpressionParser/Infix/SquareBracketExpressionParser.php @@ -0,0 +1,91 @@ +getStream(); + $lineno = $token->getLine(); + $arguments = new ArrayExpression([], $lineno); + + // slice? + $slice = false; + if ($stream->test(Token::PUNCTUATION_TYPE, ':')) { + $slice = true; + $attribute = new ConstantExpression(0, $token->getLine()); + } else { + $attribute = $parser->parseExpression(); + } + + if ($stream->nextIf(Token::PUNCTUATION_TYPE, ':')) { + $slice = true; + } + + if ($slice) { + if ($stream->test(Token::PUNCTUATION_TYPE, ']')) { + $length = new ConstantExpression(null, $token->getLine()); + } else { + $length = $parser->parseExpression(); + } + + $filter = $parser->getFilter('slice', $token->getLine()); + $arguments = new Nodes([$attribute, $length]); + $filter = new ($filter->getNodeClass())($expr, $filter, $arguments, $token->getLine()); + + $stream->expect(Token::PUNCTUATION_TYPE, ']'); + + return $filter; + } + + $stream->expect(Token::PUNCTUATION_TYPE, ']'); + + return new GetAttrExpression($expr, $attribute, $arguments, Template::ARRAY_CALL, $lineno); + } + + public function getName(): string + { + return '['; + } + + public function getDescription(): string + { + return 'Array access'; + } + + public function getPrecedence(): int + { + return 512; + } + + public function getAssociativity(): InfixAssociativity + { + return InfixAssociativity::Left; + } +} diff --git a/novaconium/vendor/twig/src/ExpressionParser/InfixAssociativity.php b/novaconium/vendor/twig/src/ExpressionParser/InfixAssociativity.php new file mode 100644 index 0000000..3aeccce --- /dev/null +++ b/novaconium/vendor/twig/src/ExpressionParser/InfixAssociativity.php @@ -0,0 +1,18 @@ + + */ +class PrecedenceChange +{ + public function __construct( + private string $package, + private string $version, + private int $newPrecedence, + ) { + } + + public function getPackage(): string + { + return $this->package; + } + + public function getVersion(): string + { + return $this->version; + } + + public function getNewPrecedence(): int + { + return $this->newPrecedence; + } +} diff --git a/novaconium/vendor/twig/src/ExpressionParser/Prefix/GroupingExpressionParser.php b/novaconium/vendor/twig/src/ExpressionParser/Prefix/GroupingExpressionParser.php new file mode 100644 index 0000000..95a1176 --- /dev/null +++ b/novaconium/vendor/twig/src/ExpressionParser/Prefix/GroupingExpressionParser.php @@ -0,0 +1,88 @@ +getStream(); + $expr = $parser->parseExpression($this->getPrecedence()); + + if ($stream->nextIf(Token::PUNCTUATION_TYPE, ')')) { + if (!$stream->test(Token::OPERATOR_TYPE, '=>')) { + return $expr->setExplicitParentheses(); + } + + return new ListExpression([self::toAssignContextVariable($expr)], $token->getLine()); + } + + // determine if we are parsing an arrow function arguments + if (!$stream->test(Token::PUNCTUATION_TYPE, ',')) { + $stream->expect(Token::PUNCTUATION_TYPE, ')', 'An opened parenthesis is not properly closed'); + } + + $names = [$expr]; + while (true) { + if ($stream->nextIf(Token::PUNCTUATION_TYPE, ')')) { + break; + } + $stream->expect(Token::PUNCTUATION_TYPE, ','); + $token = $stream->expect(Token::NAME_TYPE); + $names[] = new ContextVariable($token->getValue(), $token->getLine()); + } + + if (!$stream->test(Token::OPERATOR_TYPE, '=>')) { + throw new SyntaxError('A list of variables must be followed by an arrow.', $stream->getCurrent()->getLine(), $stream->getSourceContext()); + } + + return new ListExpression(array_map(self::toAssignContextVariable(...), $names), $token->getLine()); + } + + private static function toAssignContextVariable(AbstractExpression $expr): AssignContextVariable + { + if (!$expr instanceof ContextVariable) { + throw new SyntaxError('A list must only contain variables.', $expr->getTemplateLine(), $expr->getSourceContext()); + } + + return $expr instanceof AssignContextVariable ? $expr : new AssignContextVariable($expr->getAttribute('name'), $expr->getTemplateLine()); + } + + public function getName(): string + { + return '('; + } + + public function getDescription(): string + { + return 'Explicit group expression (a)'; + } + + public function getPrecedence(): int + { + return 0; + } +} diff --git a/novaconium/vendor/twig/src/ExpressionParser/Prefix/LiteralExpressionParser.php b/novaconium/vendor/twig/src/ExpressionParser/Prefix/LiteralExpressionParser.php new file mode 100644 index 0000000..b467d1a --- /dev/null +++ b/novaconium/vendor/twig/src/ExpressionParser/Prefix/LiteralExpressionParser.php @@ -0,0 +1,233 @@ +getStream(); + switch (true) { + case $token->test(Token::NAME_TYPE): + $stream->next(); + switch ($token->getValue()) { + case 'true': + case 'TRUE': + return new ConstantExpression(true, $token->getLine()); + + case 'false': + case 'FALSE': + return new ConstantExpression(false, $token->getLine()); + + case 'none': + case 'NONE': + case 'null': + case 'NULL': + return new ConstantExpression(null, $token->getLine()); + + default: + return new ContextVariable($token->getValue(), $token->getLine()); + } + + // no break + case $token->test(Token::NUMBER_TYPE): + $stream->next(); + + return new ConstantExpression($token->getValue(), $token->getLine()); + + case $token->test(Token::STRING_TYPE): + case $token->test(Token::INTERPOLATION_START_TYPE): + return $this->parseStringExpression($parser); + + case $token->test(Token::PUNCTUATION_TYPE): + // In 4.0, we should always return the node or throw an error for default + if ($node = match ($token->getValue()) { + '{' => $this->parseMappingExpression($parser), + default => null, + }) { + return $node; + } + + // no break + case $token->test(Token::OPERATOR_TYPE): + if ('[' === $token->getValue()) { + return $this->parseSequenceExpression($parser); + } + + if (preg_match(Lexer::REGEX_NAME, $token->getValue(), $matches) && $matches[0] == $token->getValue()) { + // in this context, string operators are variable names + $stream->next(); + + return new ContextVariable($token->getValue(), $token->getLine()); + } + + // no break + default: + throw new SyntaxError(\sprintf('Unexpected token "%s" of value "%s".', $token->toEnglish(), $token->getValue()), $token->getLine(), $stream->getSourceContext()); + } + } + + public function getName(): string + { + return 'literal'; + } + + public function getOperatorTokens(): array + { + return []; + } + + public function getDescription(): string + { + return 'A literal value (boolean, string, number, sequence, mapping, ...)'; + } + + public function getPrecedence(): int + { + // not used + return 0; + } + + private function parseStringExpression(Parser $parser) + { + $stream = $parser->getStream(); + + $nodes = []; + // a string cannot be followed by another string in a single expression + $nextCanBeString = true; + while (true) { + if ($nextCanBeString && $token = $stream->nextIf(Token::STRING_TYPE)) { + $nodes[] = new ConstantExpression($token->getValue(), $token->getLine()); + $nextCanBeString = false; + } elseif ($stream->nextIf(Token::INTERPOLATION_START_TYPE)) { + $nodes[] = $parser->parseExpression(); + $stream->expect(Token::INTERPOLATION_END_TYPE); + $nextCanBeString = true; + } else { + break; + } + } + + $expr = array_shift($nodes); + foreach ($nodes as $node) { + $expr = new ConcatBinary($expr, $node, $node->getTemplateLine()); + } + + return $expr; + } + + private function parseSequenceExpression(Parser $parser) + { + $stream = $parser->getStream(); + $stream->expect(Token::OPERATOR_TYPE, '[', 'A sequence element was expected'); + + $node = new ArrayExpression([], $stream->getCurrent()->getLine()); + $first = true; + while (!$stream->test(Token::PUNCTUATION_TYPE, ']')) { + if (!$first) { + $stream->expect(Token::PUNCTUATION_TYPE, ',', 'A sequence element must be followed by a comma'); + + // trailing ,? + if ($stream->test(Token::PUNCTUATION_TYPE, ']')) { + break; + } + } + $first = false; + + // Check for empty slots (comma with no expression) + if ($stream->test(Token::PUNCTUATION_TYPE, ',')) { + $node->addElement(new EmptyExpression($stream->getCurrent()->getLine())); + } else { + $node->addElement($parser->parseExpression()); + } + } + $stream->expect(Token::PUNCTUATION_TYPE, ']', 'An opened sequence is not properly closed'); + + return $node; + } + + private function parseMappingExpression(Parser $parser) + { + $stream = $parser->getStream(); + $stream->expect(Token::PUNCTUATION_TYPE, '{', 'A mapping element was expected'); + + $node = new ArrayExpression([], $stream->getCurrent()->getLine()); + $first = true; + while (!$stream->test(Token::PUNCTUATION_TYPE, '}')) { + if (!$first) { + $stream->expect(Token::PUNCTUATION_TYPE, ',', 'A mapping value must be followed by a comma'); + + // trailing ,? + if ($stream->test(Token::PUNCTUATION_TYPE, '}')) { + break; + } + } + $first = false; + + if ($stream->test(Token::OPERATOR_TYPE, '...')) { + $node->addElement($parser->parseExpression()); + + continue; + } + + // a mapping key can be: + // + // * a number -- 12 + // * a string -- 'a' + // * a name, which is equivalent to a string -- a + // * an expression, which must be enclosed in parentheses -- (1 + 2) + if ($token = $stream->nextIf(Token::NAME_TYPE)) { + $key = new ConstantExpression($token->getValue(), $token->getLine()); + + // {a} is a shortcut for {a:a} + if ($stream->test(Token::PUNCTUATION_TYPE, [',', '}'])) { + $value = new ContextVariable($key->getAttribute('value'), $key->getTemplateLine()); + $node->addElement($value, $key); + continue; + } + } elseif (($token = $stream->nextIf(Token::STRING_TYPE)) || $token = $stream->nextIf(Token::NUMBER_TYPE)) { + $key = new ConstantExpression($token->getValue(), $token->getLine()); + } elseif ($stream->test(Token::OPERATOR_TYPE, '(')) { + $key = $parser->parseExpression(); + } else { + $current = $stream->getCurrent(); + + throw new SyntaxError(\sprintf('A mapping key must be a quoted string, a number, a name, or an expression enclosed in parentheses (unexpected token "%s" of value "%s".', $current->toEnglish(), $current->getValue()), $current->getLine(), $stream->getSourceContext()); + } + + $stream->expect(Token::PUNCTUATION_TYPE, ':', 'A mapping key must be followed by a colon (:)'); + $value = $parser->parseExpression(); + + $node->addElement($value, $key); + } + $stream->expect(Token::PUNCTUATION_TYPE, '}', 'An opened mapping is not properly closed'); + + return $node; + } +} diff --git a/novaconium/vendor/twig/src/ExpressionParser/Prefix/UnaryOperatorExpressionParser.php b/novaconium/vendor/twig/src/ExpressionParser/Prefix/UnaryOperatorExpressionParser.php new file mode 100644 index 0000000..8013900 --- /dev/null +++ b/novaconium/vendor/twig/src/ExpressionParser/Prefix/UnaryOperatorExpressionParser.php @@ -0,0 +1,72 @@ + */ + private string $nodeClass, + private string $name, + private int $precedence, + private ?PrecedenceChange $precedenceChange = null, + private ?string $description = null, + private array $aliases = [], + private ?int $operandPrecedence = null, + ) { + } + + /** + * @return AbstractUnary + */ + public function parse(Parser $parser, Token $token): AbstractExpression + { + return new ($this->nodeClass)($parser->parseExpression($this->operandPrecedence ?? $this->precedence), $token->getLine()); + } + + public function getName(): string + { + return $this->name; + } + + public function getDescription(): string + { + return $this->description ?? ''; + } + + public function getPrecedence(): int + { + return $this->precedence; + } + + public function getPrecedenceChange(): ?PrecedenceChange + { + return $this->precedenceChange; + } + + public function getAliases(): array + { + return $this->aliases; + } +} diff --git a/novaconium/vendor/twig/src/ExpressionParser/PrefixExpressionParserInterface.php b/novaconium/vendor/twig/src/ExpressionParser/PrefixExpressionParserInterface.php new file mode 100644 index 0000000..8e39dad --- /dev/null +++ b/novaconium/vendor/twig/src/ExpressionParser/PrefixExpressionParserInterface.php @@ -0,0 +1,25 @@ +getFileName(); + if (!is_file($filename)) { + return 0; + } + + $lastModified = filemtime($filename); + + // Track modifications of the runtime class if it exists and follows the naming convention + if (str_ends_with($filename, 'Extension.php') && is_file($filename = substr($filename, 0, -13).'Runtime.php')) { + $lastModified = max($lastModified, filemtime($filename)); + } + + return $lastModified; + } +} diff --git a/novaconium/vendor/twig/src/Extension/AttributeExtension.php b/novaconium/vendor/twig/src/Extension/AttributeExtension.php new file mode 100644 index 0000000..623fd4f --- /dev/null +++ b/novaconium/vendor/twig/src/Extension/AttributeExtension.php @@ -0,0 +1,179 @@ + + */ +final class AttributeExtension extends AbstractExtension +{ + private array $filters; + private array $functions; + private array $tests; + + /** + * Use a runtime class using PHP attributes to define filters, functions, and tests. + * + * @param class-string $class + */ + public function __construct(private string $class) + { + } + + /** + * @return class-string + */ + public function getClass(): string + { + return $this->class; + } + + public function getFilters(): array + { + if (!isset($this->filters)) { + $this->initFromAttributes(); + } + + return $this->filters; + } + + public function getFunctions(): array + { + if (!isset($this->functions)) { + $this->initFromAttributes(); + } + + return $this->functions; + } + + public function getTests(): array + { + if (!isset($this->tests)) { + $this->initFromAttributes(); + } + + return $this->tests; + } + + public function getLastModified(): int + { + return max( + filemtime(__FILE__), + is_file($filename = (new \ReflectionClass($this->getClass()))->getFileName()) ? filemtime($filename) : 0, + ); + } + + private function initFromAttributes(): void + { + $filters = $functions = $tests = []; + $reflectionClass = new \ReflectionClass($this->getClass()); + foreach ($reflectionClass->getMethods() as $method) { + foreach ($method->getAttributes(AsTwigFilter::class) as $reflectionAttribute) { + /** @var AsTwigFilter $attribute */ + $attribute = $reflectionAttribute->newInstance(); + + $callable = new TwigFilter($attribute->name, [$reflectionClass->name, $method->getName()], [ + 'needs_context' => $attribute->needsContext ?? false, + 'needs_environment' => $attribute->needsEnvironment ?? $this->needsEnvironment($method), + 'needs_charset' => $attribute->needsCharset ?? false, + 'needs_is_sandboxed' => $attribute->needsIsSandboxed ?? false, + 'is_variadic' => $method->isVariadic(), + 'is_safe' => $attribute->isSafe, + 'is_safe_callback' => $attribute->isSafeCallback, + 'pre_escape' => $attribute->preEscape, + 'preserves_safety' => $attribute->preservesSafety, + 'always_allowed_in_sandbox' => $attribute->alwaysAllowedInSandbox ?? false, + 'deprecation_info' => $attribute->deprecationInfo, + ]); + + if ($callable->getMinimalNumberOfRequiredArguments() > $method->getNumberOfParameters()) { + throw new \LogicException(\sprintf('"%s::%s()" needs at least %d arguments to be used AsTwigFilter, but only %d defined.', $reflectionClass->getName(), $method->getName(), $callable->getMinimalNumberOfRequiredArguments(), $method->getNumberOfParameters())); + } + + $filters[$attribute->name] = $callable; + } + + foreach ($method->getAttributes(AsTwigFunction::class) as $reflectionAttribute) { + /** @var AsTwigFunction $attribute */ + $attribute = $reflectionAttribute->newInstance(); + + $callable = new TwigFunction($attribute->name, [$reflectionClass->name, $method->getName()], [ + 'needs_context' => $attribute->needsContext ?? false, + 'needs_environment' => $attribute->needsEnvironment ?? $this->needsEnvironment($method), + 'needs_charset' => $attribute->needsCharset ?? false, + 'needs_is_sandboxed' => $attribute->needsIsSandboxed ?? false, + 'is_variadic' => $method->isVariadic(), + 'is_safe' => $attribute->isSafe, + 'is_safe_callback' => $attribute->isSafeCallback, + 'always_allowed_in_sandbox' => $attribute->alwaysAllowedInSandbox ?? false, + 'deprecation_info' => $attribute->deprecationInfo, + ]); + + if ($callable->getMinimalNumberOfRequiredArguments() > $method->getNumberOfParameters()) { + throw new \LogicException(\sprintf('"%s::%s()" needs at least %d arguments to be used AsTwigFunction, but only %d defined.', $reflectionClass->getName(), $method->getName(), $callable->getMinimalNumberOfRequiredArguments(), $method->getNumberOfParameters())); + } + + $functions[$attribute->name] = $callable; + } + + foreach ($method->getAttributes(AsTwigTest::class) as $reflectionAttribute) { + /** @var AsTwigTest $attribute */ + $attribute = $reflectionAttribute->newInstance(); + + $callable = new TwigTest($attribute->name, [$reflectionClass->name, $method->getName()], [ + 'needs_context' => $attribute->needsContext ?? false, + 'needs_environment' => $attribute->needsEnvironment ?? $this->needsEnvironment($method), + 'needs_charset' => $attribute->needsCharset ?? false, + 'needs_is_sandboxed' => $attribute->needsIsSandboxed ?? false, + 'is_variadic' => $method->isVariadic(), + 'always_allowed_in_sandbox' => $attribute->alwaysAllowedInSandbox ?? false, + 'deprecation_info' => $attribute->deprecationInfo, + ]); + + if ($callable->getMinimalNumberOfRequiredArguments() > $method->getNumberOfParameters()) { + throw new \LogicException(\sprintf('"%s::%s()" needs at least %d arguments to be used AsTwigTest, but only %d defined.', $reflectionClass->getName(), $method->getName(), $callable->getMinimalNumberOfRequiredArguments(), $method->getNumberOfParameters())); + } + + $tests[$attribute->name] = $callable; + } + } + + // Assign all at the end to avoid inconsistent state in case of exception + $this->filters = array_values($filters); + $this->functions = array_values($functions); + $this->tests = array_values($tests); + } + + /** + * Detect if the first argument of the method is the environment. + */ + private function needsEnvironment(\ReflectionFunctionAbstract $function): bool + { + if (!$parameters = $function->getParameters()) { + return false; + } + + return $parameters[0]->getType() instanceof \ReflectionNamedType + && Environment::class === $parameters[0]->getType()->getName() + && !$parameters[0]->isVariadic(); + } +} diff --git a/novaconium/vendor/twig/src/Extension/CoreExtension.php b/novaconium/vendor/twig/src/Extension/CoreExtension.php new file mode 100644 index 0000000..9ef620b --- /dev/null +++ b/novaconium/vendor/twig/src/Extension/CoreExtension.php @@ -0,0 +1,2223 @@ +dateFormats[0] = $format; + } + + if (null !== $dateIntervalFormat) { + $this->dateFormats[1] = $dateIntervalFormat; + } + } + + /** + * Gets the default format to be used by the date filter. + * + * @return array The default date format string and the default date interval format string + */ + public function getDateFormat() + { + return $this->dateFormats; + } + + /** + * Sets the default timezone to be used by the date filter. + * + * @param \DateTimeZone|string $timezone The default timezone string or a \DateTimeZone object + */ + public function setTimezone($timezone) + { + $this->timezone = $timezone instanceof \DateTimeZone ? $timezone : new \DateTimeZone($timezone); + } + + /** + * Gets the default timezone to be used by the date filter. + * + * @return \DateTimeZone The default timezone currently in use + */ + public function getTimezone() + { + if (null === $this->timezone) { + $this->timezone = new \DateTimeZone(date_default_timezone_get()); + } + + return $this->timezone; + } + + /** + * Sets the default format to be used by the number_format filter. + * + * @param int $decimal the number of decimal places to use + * @param string $decimalPoint the character(s) to use for the decimal point + * @param string $thousandSep the character(s) to use for the thousands separator + */ + public function setNumberFormat($decimal, $decimalPoint, $thousandSep) + { + $this->numberFormat = [$decimal, $decimalPoint, $thousandSep]; + } + + /** + * Get the default format used by the number_format filter. + * + * @return array The arguments for number_format() + */ + public function getNumberFormat() + { + return $this->numberFormat; + } + + public function getTokenParsers(): array + { + return [ + new ApplyTokenParser(), + new ForTokenParser(), + new IfTokenParser(), + new ExtendsTokenParser(), + new IncludeTokenParser(), + new BlockTokenParser(), + new UseTokenParser(), + new MacroTokenParser(), + new ImportTokenParser(), + new FromTokenParser(), + new SetTokenParser(), + new TypesTokenParser(), + new FlushTokenParser(), + new DoTokenParser(), + new EmbedTokenParser(), + new WithTokenParser(), + new DeprecatedTokenParser(), + new GuardTokenParser(), + ]; + } + + public function getFilters(): array + { + return [ + // formatting filters + new TwigFilter('date', [$this, 'formatDate']), + new TwigFilter('date_modify', [$this, 'modifyDate']), + new TwigFilter('format', [self::class, 'sprintf']), + new TwigFilter('replace', [self::class, 'replace']), + new TwigFilter('number_format', [$this, 'formatNumber']), + new TwigFilter('abs', 'abs'), + new TwigFilter('round', [self::class, 'round']), + + // encoding + new TwigFilter('url_encode', [self::class, 'urlencode']), + new TwigFilter('json_encode', 'json_encode'), + new TwigFilter('convert_encoding', [self::class, 'convertEncoding']), + + // string filters + new TwigFilter('title', [self::class, 'titleCase'], ['needs_charset' => true]), + new TwigFilter('capitalize', [self::class, 'capitalize'], ['needs_charset' => true]), + new TwigFilter('upper', [self::class, 'upper'], ['needs_charset' => true]), + new TwigFilter('lower', [self::class, 'lower'], ['needs_charset' => true]), + new TwigFilter('striptags', [self::class, 'striptags']), + new TwigFilter('trim', [self::class, 'trim']), + new TwigFilter('nl2br', [self::class, 'nl2br'], ['pre_escape' => 'html', 'is_safe' => ['html']]), + new TwigFilter('spaceless', [self::class, 'spaceless'], ['pre_escape' => 'html', 'is_safe' => ['html'], 'deprecation_info' => new DeprecatedCallableInfo('twig/twig', '3.12')]), + + // array helpers + new TwigFilter('join', [self::class, 'join']), + new TwigFilter('split', [self::class, 'split'], ['needs_charset' => true]), + new TwigFilter('sort', [self::class, 'sort'], ['needs_environment' => true, 'needs_is_sandboxed' => true]), + new TwigFilter('merge', [self::class, 'merge']), + new TwigFilter('batch', [self::class, 'batch']), + new TwigFilter('column', [self::class, 'column'], ['needs_environment' => true, 'needs_is_sandboxed' => true]), + new TwigFilter('filter', [self::class, 'filter'], ['needs_environment' => true, 'needs_is_sandboxed' => true]), + new TwigFilter('map', [self::class, 'map'], ['needs_environment' => true, 'needs_is_sandboxed' => true]), + new TwigFilter('reduce', [self::class, 'reduce'], ['needs_environment' => true, 'needs_is_sandboxed' => true]), + new TwigFilter('find', [self::class, 'find'], ['needs_environment' => true, 'needs_is_sandboxed' => true]), + + // string/array filters + new TwigFilter('reverse', [self::class, 'reverse'], ['needs_charset' => true]), + new TwigFilter('shuffle', [self::class, 'shuffle'], ['needs_charset' => true]), + new TwigFilter('length', [self::class, 'length'], ['needs_charset' => true]), + new TwigFilter('slice', [self::class, 'slice'], ['needs_charset' => true]), + new TwigFilter('first', [self::class, 'first'], ['needs_charset' => true]), + new TwigFilter('last', [self::class, 'last'], ['needs_charset' => true]), + + // iteration and runtime + new TwigFilter('default', [self::class, 'default'], ['node_class' => DefaultFilter::class]), + new TwigFilter('keys', [self::class, 'keys']), + new TwigFilter('invoke', [self::class, 'invoke']), + ]; + } + + public function getFunctions(): array + { + return [ + new TwigFunction('parent', null, ['parser_callable' => [self::class, 'parseParentFunction']]), + new TwigFunction('block', null, ['parser_callable' => [self::class, 'parseBlockFunction']]), + new TwigFunction('attribute', null, ['parser_callable' => [self::class, 'parseAttributeFunction']]), + new TwigFunction('max', 'max'), + new TwigFunction('min', 'min'), + new TwigFunction('range', 'range'), + new TwigFunction('constant', [self::class, 'constant']), + new TwigFunction('cycle', [self::class, 'cycle']), + new TwigFunction('random', [self::class, 'random'], ['needs_charset' => true]), + new TwigFunction('date', [$this, 'convertDate']), + new TwigFunction('include', [self::class, 'include'], ['needs_environment' => true, 'needs_context' => true, 'is_safe' => ['all']]), + new TwigFunction('source', [self::class, 'source'], ['needs_environment' => true, 'is_safe' => ['all']]), + new TwigFunction('enum_cases', [self::class, 'enumCases'], ['node_class' => EnumCasesFunction::class]), + new TwigFunction('enum', [self::class, 'enum'], ['node_class' => EnumFunction::class]), + ]; + } + + public function getTests(): array + { + return [ + new TwigTest('even', null, ['node_class' => EvenTest::class, 'always_allowed_in_sandbox' => true]), + new TwigTest('odd', null, ['node_class' => OddTest::class, 'always_allowed_in_sandbox' => true]), + new TwigTest('defined', null, ['node_class' => DefinedTest::class, 'always_allowed_in_sandbox' => true]), + new TwigTest('same as', null, ['node_class' => SameasTest::class, 'one_mandatory_argument' => true, 'always_allowed_in_sandbox' => true]), + new TwigTest('none', null, ['node_class' => NullTest::class, 'always_allowed_in_sandbox' => true]), + new TwigTest('null', null, ['node_class' => NullTest::class, 'always_allowed_in_sandbox' => true]), + new TwigTest('divisible by', null, ['node_class' => DivisiblebyTest::class, 'one_mandatory_argument' => true, 'always_allowed_in_sandbox' => true]), + new TwigTest('constant', null, ['node_class' => ConstantTest::class]), + new TwigTest('empty', [self::class, 'testEmpty'], ['always_allowed_in_sandbox' => true]), + new TwigTest('iterable', 'is_iterable', ['always_allowed_in_sandbox' => true]), + new TwigTest('sequence', [self::class, 'testSequence'], ['always_allowed_in_sandbox' => true]), + new TwigTest('mapping', [self::class, 'testMapping'], ['always_allowed_in_sandbox' => true]), + new TwigTest('true', null, ['node_class' => TrueTest::class, 'always_allowed_in_sandbox' => true]), + ]; + } + + public function getNodeVisitors(): array + { + return [ + new CorrectnessNodeVisitor(), + ]; + } + + public function getExpressionParsers(): array + { + return [ + // unary operators + new UnaryOperatorExpressionParser(NotUnary::class, 'not', 50, new PrecedenceChange('twig/twig', '3.15', 70)), + new UnaryOperatorExpressionParser(SpreadUnary::class, '...', 512, description: 'Spread operator', operandPrecedence: 0), + new UnaryOperatorExpressionParser(NegUnary::class, '-', 500), + new UnaryOperatorExpressionParser(PosUnary::class, '+', 500), + + // binary operators + new BinaryOperatorExpressionParser(ElvisBinary::class, '?:', 5, InfixAssociativity::Right, description: 'Elvis operator (a ?: b)', aliases: ['? :']), + new BinaryOperatorExpressionParser(NullCoalesceBinary::class, '??', 300, InfixAssociativity::Right, new PrecedenceChange('twig/twig', '3.15', 5), description: 'Null coalescing operator (a ?? b)'), + new BinaryOperatorExpressionParser(OrBinary::class, 'or', 10), + new BinaryOperatorExpressionParser(XorBinary::class, 'xor', 12), + new BinaryOperatorExpressionParser(AndBinary::class, 'and', 15), + new BinaryOperatorExpressionParser(BitwiseOrBinary::class, 'b-or', 16), + new BinaryOperatorExpressionParser(BitwiseXorBinary::class, 'b-xor', 17), + new BinaryOperatorExpressionParser(BitwiseAndBinary::class, 'b-and', 18), + new BinaryOperatorExpressionParser(EqualBinary::class, '==', 20), + new BinaryOperatorExpressionParser(NotEqualBinary::class, '!=', 20), + new BinaryOperatorExpressionParser(SpaceshipBinary::class, '<=>', 20), + new BinaryOperatorExpressionParser(LessBinary::class, '<', 20), + new BinaryOperatorExpressionParser(GreaterBinary::class, '>', 20), + new BinaryOperatorExpressionParser(GreaterEqualBinary::class, '>=', 20), + new BinaryOperatorExpressionParser(LessEqualBinary::class, '<=', 20), + new BinaryOperatorExpressionParser(NotInBinary::class, 'not in', 20), + new BinaryOperatorExpressionParser(InBinary::class, 'in', 20), + new BinaryOperatorExpressionParser(MatchesBinary::class, 'matches', 20), + new BinaryOperatorExpressionParser(StartsWithBinary::class, 'starts with', 20), + new BinaryOperatorExpressionParser(EndsWithBinary::class, 'ends with', 20), + new BinaryOperatorExpressionParser(HasSomeBinary::class, 'has some', 20), + new BinaryOperatorExpressionParser(HasEveryBinary::class, 'has every', 20), + new BinaryOperatorExpressionParser(SameAsBinary::class, '===', 20), + new BinaryOperatorExpressionParser(NotSameAsBinary::class, '!==', 20), + new BinaryOperatorExpressionParser(RangeBinary::class, '..', 25), + new BinaryOperatorExpressionParser(AddBinary::class, '+', 30), + new BinaryOperatorExpressionParser(SubBinary::class, '-', 30), + new BinaryOperatorExpressionParser(ConcatBinary::class, '~', 40, precedenceChange: new PrecedenceChange('twig/twig', '3.15', 27)), + new BinaryOperatorExpressionParser(MulBinary::class, '*', 60), + new BinaryOperatorExpressionParser(DivBinary::class, '/', 60), + new BinaryOperatorExpressionParser(FloorDivBinary::class, '//', 60, description: 'Floor division'), + new BinaryOperatorExpressionParser(ModBinary::class, '%', 60), + new BinaryOperatorExpressionParser(PowerBinary::class, '**', 200, InfixAssociativity::Right, description: 'Exponentiation operator'), + + // ternary operator + new ConditionalTernaryExpressionParser(), + + // assignment operator + new AssignmentExpressionParser('='), + + // Twig callables + new IsExpressionParser(), + new IsNotExpressionParser(), + new FilterExpressionParser(), + new FunctionExpressionParser(), + + // get attribute operators + new DotExpressionParser(), + new SquareBracketExpressionParser(), + + // group expression + new GroupingExpressionParser(), + + // arrow function + new ArrowExpressionParser(), + + // all literals + new LiteralExpressionParser(), + ]; + } + + /** + * Cycles over a sequence. + * + * @param array|\ArrayAccess $values A non-empty sequence of values + * @param int<0, max> $position The position of the value to return in the cycle + * + * @return mixed The value at the given position in the sequence, wrapping around as needed + * + * @internal + */ + public static function cycle($values, $position): mixed + { + if (!\is_array($values)) { + if (!$values instanceof \ArrayAccess) { + throw new RuntimeError('The "cycle" function expects an array or "ArrayAccess" as first argument.'); + } + + if (!is_countable($values)) { + // To be uncommented in 4.0 + // throw new RuntimeError('The "cycle" function expects a countable sequence as first argument.'); + + trigger_deprecation('twig/twig', '3.12', 'Passing a non-countable sequence of values to "%s()" is deprecated.', __METHOD__); + + $values = self::toArray($values, false); + } + } + + if (!$count = \count($values)) { + throw new RuntimeError('The "cycle" function expects a non-empty sequence.'); + } + + return $values[$position % $count]; + } + + /** + * Returns a random value depending on the supplied parameter type: + * - a random item from a \Traversable or array + * - a random character from a string + * - a random integer between 0 and the integer parameter. + * + * @param \Traversable|array|int|float|string $values The values to pick a random item from + * @param int|null $max Maximum value used when $values is an int + * + * @return mixed A random value from the given sequence + * + * @throws RuntimeError when $values is an empty array (does not apply to an empty string which is returned as is) + * + * @internal + */ + public static function random(string $charset, $values = null, $max = null) + { + if (null === $values) { + return null === $max ? mt_rand() : mt_rand(0, (int) $max); + } + + if (\is_int($values) || \is_float($values)) { + if (null === $max) { + if ($values < 0) { + $max = 0; + $min = $values; + } else { + $max = $values; + $min = 0; + } + } else { + $min = $values; + } + + return mt_rand((int) $min, (int) $max); + } + + if (\is_string($values)) { + if ('' === $values) { + return ''; + } + + if ('UTF-8' !== $charset) { + $values = self::convertEncoding($values, 'UTF-8', $charset); + } + + // unicode version of str_split() + // split at all positions, but not after the start and not before the end + $values = preg_split('/(? $value) { + $values[$i] = self::convertEncoding($value, $charset, 'UTF-8'); + } + } + } + + if (!is_iterable($values)) { + return $values; + } + + $values = self::toArray($values); + + if (0 === \count($values)) { + throw new RuntimeError('The "random" function cannot pick from an empty sequence or mapping.'); + } + + return $values[array_rand($values, 1)]; + } + + /** + * Formats a date. + * + * {{ post.published_at|date("m/d/Y") }} + * + * @param \DateTimeInterface|\DateInterval|string|int|null $date A date, a timestamp or null to use the current time + * @param string|null $format The target format, null to use the default + * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged + */ + public function formatDate($date, $format = null, $timezone = null): string + { + if (null === $format) { + $formats = $this->getDateFormat(); + $format = $date instanceof \DateInterval ? $formats[1] : $formats[0]; + } + + if ($date instanceof \DateInterval) { + return $date->format($format); + } + + return $this->convertDate($date, $timezone)->format($format); + } + + /** + * Returns a new date object modified. + * + * {{ post.published_at|date_modify("-1day")|date("m/d/Y") }} + * + * @param \DateTimeInterface|string|int|null $date A date, a timestamp or null to use the current time + * @param string $modifier A modifier string + * + * @return \DateTime|\DateTimeImmutable + * + * @internal + */ + public function modifyDate($date, $modifier) + { + return $this->convertDate($date, false)->modify($modifier); + } + + /** + * Returns a formatted string. + * + * @param string|null $format + * + * @internal + */ + public static function sprintf($format, ...$values): string + { + return \sprintf($format ?? '', ...$values); + } + + /** + * @internal + */ + public static function dateConverter(Environment $env, $date, $format = null, $timezone = null): string + { + return $env->getExtension(self::class)->formatDate($date, $format, $timezone); + } + + /** + * Converts an input to a \DateTime instance. + * + * {% if date(user.created_at) < date('+2days') %} + * {# do something #} + * {% endif %} + * + * @param \DateTimeInterface|string|int|null $date A date, a timestamp or null to use the current time + * @param \DateTimeZone|string|false|null $timezone The target timezone, null to use the default, false to leave unchanged + * + * @return \DateTime|\DateTimeImmutable + */ + public function convertDate($date = null, $timezone = null) + { + // determine the timezone + if (false !== $timezone) { + if (null === $timezone) { + $timezone = $this->getTimezone(); + } elseif (!$timezone instanceof \DateTimeZone) { + $timezone = new \DateTimeZone($timezone); + } + } + + // immutable dates + if ($date instanceof \DateTimeImmutable) { + return false !== $timezone ? $date->setTimezone($timezone) : $date; + } + + if ($date instanceof \DateTime) { + $date = clone $date; + if (false !== $timezone) { + $date->setTimezone($timezone); + } + + return $date; + } + + if (null === $date || 'now' === $date) { + if (null === $date) { + $date = 'now'; + } + + return new \DateTime($date, false !== $timezone ? $timezone : $this->getTimezone()); + } + + $asString = (string) $date; + if (ctype_digit($asString) || ('' !== $asString && '-' === $asString[0] && ctype_digit(substr($asString, 1)))) { + $date = new \DateTime('@'.$date); + } else { + $date = new \DateTime($date); + } + + if (false !== $timezone) { + $date->setTimezone($timezone); + } + + return $date; + } + + /** + * Replaces strings within a string. + * + * @param string|null $str String to replace in + * @param array|\Traversable $from Replace values + * + * @internal + */ + public static function replace($str, $from): string + { + if (!is_iterable($from)) { + throw new RuntimeError(\sprintf('The "replace" filter expects a sequence or a mapping, got "%s".', get_debug_type($from))); + } + + return strtr($str ?? '', self::toArray($from)); + } + + /** + * Rounds a number. + * + * @param int|float|string|null $value The value to round + * @param int|float $precision The rounding precision + * @param 'common'|'ceil'|'floor' $method The method to use for rounding + * + * @return float The rounded number + * + * @internal + */ + public static function round($value, $precision = 0, $method = 'common') + { + $value = (float) $value; + + if ('common' === $method) { + return round($value, $precision); + } + + if ('ceil' !== $method && 'floor' !== $method) { + throw new RuntimeError('The "round" filter only supports the "common", "ceil", and "floor" methods.'); + } + + return $method($value * 10 ** $precision) / 10 ** $precision; + } + + /** + * Formats a number. + * + * All of the formatting options can be left null, in that case the defaults will + * be used. Supplying any of the parameters will override the defaults set in the + * environment object. + * + * @param mixed $number A float/int/string of the number to format + * @param int|null $decimal the number of decimal points to display + * @param string|null $decimalPoint the character(s) to use for the decimal point + * @param string|null $thousandSep the character(s) to use for the thousands separator + */ + public function formatNumber($number, $decimal = null, $decimalPoint = null, $thousandSep = null): string + { + $defaults = $this->getNumberFormat(); + if (null === $decimal) { + $decimal = $defaults[0]; + } + + if (null === $decimalPoint) { + $decimalPoint = $defaults[1]; + } + + if (null === $thousandSep) { + $thousandSep = $defaults[2]; + } + + return number_format((float) $number, $decimal, $decimalPoint, $thousandSep); + } + + /** + * URL encodes (RFC 3986) a string as a path segment or an array as a query string. + * + * @param string|array|null $url A URL or an array of query parameters + * + * @internal + */ + public static function urlencode($url): string + { + if (\is_array($url)) { + return http_build_query($url, '', '&', \PHP_QUERY_RFC3986); + } + + return rawurlencode($url ?? ''); + } + + /** + * Merges any number of arrays or Traversable objects. + * + * {% set items = { 'apple': 'fruit', 'orange': 'fruit' } %} + * + * {% set items = items|merge({ 'peugeot': 'car' }, { 'banana': 'fruit' }) %} + * + * {# items now contains { 'apple': 'fruit', 'orange': 'fruit', 'peugeot': 'car', 'banana': 'fruit' } #} + * + * @param array|\Traversable ...$arrays Any number of arrays or Traversable objects to merge + * + * @internal + */ + public static function merge(...$arrays): array + { + $result = []; + + foreach ($arrays as $argNumber => $array) { + if (!is_iterable($array)) { + throw new RuntimeError(\sprintf('The "merge" filter expects a sequence or a mapping, got "%s" for argument %d.', get_debug_type($array), $argNumber + 1)); + } + + $result = array_merge($result, self::toArray($array)); + } + + return $result; + } + + /** + * Slices a variable. + * + * @param mixed $item A variable + * @param int $start Start of the slice + * @param int $length Size of the slice + * @param bool $preserveKeys Whether to preserve key or not (when the input is an array) + * + * @return mixed The sliced variable + * + * @internal + */ + public static function slice(string $charset, $item, $start, $length = null, $preserveKeys = false) + { + if ($item instanceof \Traversable) { + while ($item instanceof \IteratorAggregate) { + $item = $item->getIterator(); + } + + if ($start >= 0 && $length >= 0 && $item instanceof \Iterator) { + try { + return iterator_to_array(new \LimitIterator($item, $start, $length ?? -1), $preserveKeys); + } catch (\OutOfBoundsException $e) { + return []; + } + } + + $item = iterator_to_array($item, $preserveKeys); + } + + if (\is_array($item)) { + return \array_slice($item, $start, $length, $preserveKeys); + } + + return mb_substr((string) $item, $start, $length, $charset); + } + + /** + * Returns the first element of the item. + * + * @param mixed $item A variable + * + * @return mixed The first element of the item + * + * @internal + */ + public static function first(string $charset, $item) + { + $elements = self::slice($charset, $item, 0, 1, false); + + return \is_string($elements) ? $elements : current($elements); + } + + /** + * Returns the last element of the item. + * + * @param mixed $item A variable + * + * @return mixed The last element of the item + * + * @internal + */ + public static function last(string $charset, $item) + { + $elements = self::slice($charset, $item, -1, 1, false); + + return \is_string($elements) ? $elements : current($elements); + } + + /** + * Joins the values to a string. + * + * The separators between elements are empty strings per default, you can define them with the optional parameters. + * + * {{ [1, 2, 3]|join(', ', ' and ') }} + * {# returns 1, 2 and 3 #} + * + * {{ [1, 2, 3]|join('|') }} + * {# returns 1|2|3 #} + * + * {{ [1, 2, 3]|join }} + * {# returns 123 #} + * + * @param iterable|array|string|float|int|bool|null $value An array + * @param string $glue The separator + * @param string|null $and The separator for the last pair + * + * @internal + */ + public static function join($value, $glue = '', $and = null): string + { + if (!is_iterable($value)) { + $value = (array) $value; + } + + $value = self::toArray($value, false); + + if (0 === \count($value)) { + return ''; + } + + if (null === $and || $and === $glue) { + return implode($glue, $value); + } + + if (1 === \count($value)) { + return $value[0]; + } + + return implode($glue, \array_slice($value, 0, -1)).$and.$value[\count($value) - 1]; + } + + /** + * Splits the string into an array. + * + * {{ "one,two,three"|split(',') }} + * {# returns [one, two, three] #} + * + * {{ "one,two,three,four,five"|split(',', 3) }} + * {# returns [one, two, "three,four,five"] #} + * + * {{ "123"|split('') }} + * {# returns [1, 2, 3] #} + * + * {{ "aabbcc"|split('', 2) }} + * {# returns [aa, bb, cc] #} + * + * @param string|null $value A string + * @param string $delimiter The delimiter + * @param int|null $limit The limit + * + * @internal + */ + public static function split(string $charset, $value, $delimiter, $limit = null): array + { + $value = $value ?? ''; + + if ('' !== $delimiter) { + return null === $limit ? explode($delimiter, $value) : explode($delimiter, $value, $limit); + } + + if ($limit <= 1) { + return preg_split('/(?getIterator(); + } + + $keys = []; + if ($array instanceof \Iterator) { + $array->rewind(); + while ($array->valid()) { + $keys[] = $array->key(); + $array->next(); + } + + return $keys; + } + + foreach ($array as $key => $item) { + $keys[] = $key; + } + + return $keys; + } + + if (!\is_array($array)) { + return []; + } + + return array_keys($array); + } + + /** + * Invokes a callable. + * + * @internal + */ + public static function invoke(\Closure $arrow, ...$arguments): mixed + { + return $arrow(...$arguments); + } + + /** + * Reverses a variable. + * + * @param array|\Traversable|string|null $item An array, a \Traversable instance, or a string + * @param bool $preserveKeys Whether to preserve key or not + * + * @return mixed The reversed input + * + * @internal + */ + public static function reverse(string $charset, $item, $preserveKeys = false) + { + if ($item instanceof \Traversable) { + return array_reverse(iterator_to_array($item), $preserveKeys); + } + + if (\is_array($item)) { + return array_reverse($item, $preserveKeys); + } + + $string = (string) $item; + + if ('UTF-8' !== $charset) { + $string = self::convertEncoding($string, 'UTF-8', $charset); + } + + preg_match_all('/./us', $string, $matches); + + $string = implode('', array_reverse($matches[0])); + + if ('UTF-8' !== $charset) { + $string = self::convertEncoding($string, $charset, 'UTF-8'); + } + + return $string; + } + + /** + * Shuffles an array, a \Traversable instance, or a string. + * The function does not preserve keys. + * + * @param array|\Traversable|string|null $item + * + * @internal + */ + public static function shuffle(string $charset, $item) + { + if (\is_string($item)) { + if ('UTF-8' !== $charset) { + $item = self::convertEncoding($item, 'UTF-8', $charset); + } + + $item = preg_split('/(? string + if (\is_int($a) && \is_string($b)) { + $bTrim = trim($b, " \t\n\r\v\f"); + if (!is_numeric($bTrim)) { + return (string) $a <=> $b; + } + if ((int) $bTrim == $bTrim) { + return $a <=> (int) $bTrim; + } + + return (float) $a <=> (float) $bTrim; + } + if (\is_string($a) && \is_int($b)) { + $aTrim = trim($a, " \t\n\r\v\f"); + if (!is_numeric($aTrim)) { + return $a <=> (string) $b; + } + if ((int) $aTrim == $aTrim) { + return (int) $aTrim <=> $b; + } + + return (float) $aTrim <=> (float) $b; + } + + // float <=> string + if (\is_float($a) && \is_string($b)) { + if (is_nan($a)) { + return 1; + } + $bTrim = trim($b, " \t\n\r\v\f"); + if (!is_numeric($bTrim)) { + return (string) $a <=> $b; + } + + return $a <=> (float) $bTrim; + } + if (\is_string($a) && \is_float($b)) { + if (is_nan($b)) { + return 1; + } + $aTrim = trim($a, " \t\n\r\v\f"); + if (!is_numeric($aTrim)) { + return $a <=> (string) $b; + } + + return (float) $aTrim <=> $b; + } + + // fallback to <=> + return $a <=> $b; + } + + /** + * @throws RuntimeError When an invalid pattern is used + * + * @internal + */ + public static function matches(string $regexp, ?string $str): int + { + set_error_handler(static function ($t, $m) use ($regexp) { + throw new RuntimeError(\sprintf('Regexp "%s" passed to "matches" is not valid', $regexp).substr($m, 12)); + }); + try { + return preg_match($regexp, $str ?? ''); + } finally { + restore_error_handler(); + } + } + + /** + * Returns a trimmed string. + * + * @param string|\Stringable|null $string + * @param string|null $characterMask + * @param string $side left, right, or both + * + * @throws RuntimeError When an invalid trimming side is used + * + * @internal + */ + public static function trim($string, $characterMask = null, $side = 'both'): string|\Stringable + { + if (null === $characterMask) { + $characterMask = self::DEFAULT_TRIM_CHARS; + } + + $trimmed = match ($side) { + 'both' => trim($string ?? '', $characterMask), + 'left' => ltrim($string ?? '', $characterMask), + 'right' => rtrim($string ?? '', $characterMask), + default => throw new RuntimeError('Trimming side must be "left", "right" or "both".'), + }; + + // trimming a safe string with the default character mask always returns a safe string (independently of the context) + return $string instanceof Markup && self::DEFAULT_TRIM_CHARS === $characterMask ? new Markup($trimmed, $string->getCharset()) : $trimmed; + } + + /** + * Inserts HTML line breaks before all newlines in a string. + * + * @param string|null $string + * + * @internal + */ + public static function nl2br($string): string + { + return nl2br($string ?? ''); + } + + /** + * Removes whitespaces between HTML tags. + * + * @param string|null $content + * + * @internal + */ + public static function spaceless($content): string + { + return trim(preg_replace('/>\s+<', $content ?? '')); + } + + /** + * @param string|null $string + * @param string $to + * @param string $from + * + * @internal + */ + public static function convertEncoding($string, $to, $from): string + { + if (!\function_exists('iconv')) { + throw new RuntimeError('Unable to convert encoding: required function iconv() does not exist. You should install ext-iconv or symfony/polyfill-iconv.'); + } + + return iconv($from, $to, $string ?? ''); + } + + /** + * Returns the length of a variable. + * + * @param mixed $thing A variable + * + * @internal + */ + public static function length(string $charset, $thing): int + { + if (null === $thing) { + return 0; + } + + if (\is_scalar($thing)) { + return mb_strlen($thing, $charset); + } + + if ($thing instanceof \Countable || \is_array($thing) || $thing instanceof \SimpleXMLElement) { + return \count($thing); + } + + if ($thing instanceof \Traversable) { + return iterator_count($thing); + } + + if ($thing instanceof \Stringable) { + return mb_strlen((string) $thing, $charset); + } + + return 1; + } + + /** + * Converts a string to uppercase. + * + * @param string|null $string A string + * + * @internal + */ + public static function upper(string $charset, $string): string + { + return mb_strtoupper($string ?? '', $charset); + } + + /** + * Converts a string to lowercase. + * + * @param string|null $string A string + * + * @internal + */ + public static function lower(string $charset, $string): string + { + return mb_strtolower($string ?? '', $charset); + } + + /** + * Strips HTML and PHP tags from a string. + * + * @param string|null $string + * @param string[]|string|null $allowable_tags + * + * @internal + */ + public static function striptags($string, $allowable_tags = null): string + { + return strip_tags($string ?? '', $allowable_tags); + } + + /** + * Returns a titlecased string. + * + * @param string|null $string A string + * + * @internal + */ + public static function titleCase(string $charset, $string): string + { + return mb_convert_case($string ?? '', \MB_CASE_TITLE, $charset); + } + + /** + * Returns a capitalized string. + * + * @param string|null $string A string + * + * @internal + */ + public static function capitalize(string $charset, $string): string + { + return mb_strtoupper(mb_substr($string ?? '', 0, 1, $charset), $charset).mb_strtolower(mb_substr($string ?? '', 1, null, $charset), $charset); + } + + /** + * @internal + * + * to be removed in 4.0 + */ + public static function callMacro(Template $template, string $method, array $args, int $lineno, array $context, Source $source) + { + if (!method_exists($template, $method)) { + $parent = $template; + while ($parent = $parent->getParent($context)) { + if (method_exists($parent, $method)) { + return $parent->$method(...$args); + } + } + + throw new RuntimeError(\sprintf('Macro "%s" is not defined in template "%s".', substr($method, \strlen('macro_')), $template->getTemplateName()), $lineno, $source); + } + + return $template->$method(...$args); + } + + /** + * @template TSequence + * + * @param TSequence $seq + * + * @return ($seq is iterable ? TSequence : array{}) + * + * @internal + */ + public static function ensureTraversable($seq) + { + if (is_iterable($seq)) { + return $seq; + } + + return []; + } + + /** + * @internal + */ + public static function toArray($seq, $preserveKeys = true) + { + if ($seq instanceof \Traversable) { + return iterator_to_array($seq, $preserveKeys); + } + + if (!\is_array($seq)) { + return $seq; + } + + return $preserveKeys ? $seq : array_values($seq); + } + + /** + * Checks if a variable is empty. + * + * {# evaluates to true if the foo variable is null, false, or the empty string #} + * {% if foo is empty %} + * {# ... #} + * {% endif %} + * + * @param mixed $value A variable + * + * @internal + */ + public static function testEmpty($value): bool + { + if ($value instanceof \Countable) { + return 0 === \count($value); + } + + if ($value instanceof \Traversable) { + return !iterator_count($value); + } + + if ($value instanceof \Stringable) { + return '' === (string) $value; + } + + return '' === $value || false === $value || null === $value || [] === $value; + } + + /** + * Checks if a variable is a sequence. + * + * {# evaluates to true if the foo variable is a sequence #} + * {% if foo is sequence %} + * {# ... #} + * {% endif %} + * + * @internal + */ + public static function testSequence($value): bool + { + if ($value instanceof \ArrayObject) { + $value = $value->getArrayCopy(); + } + + if ($value instanceof \Traversable) { + $value = iterator_to_array($value); + } + + return \is_array($value) && array_is_list($value); + } + + /** + * Checks if a variable is a mapping. + * + * {# evaluates to true if the foo variable is a mapping #} + * {% if foo is mapping %} + * {# ... #} + * {% endif %} + * + * @internal + */ + public static function testMapping($value): bool + { + if ($value instanceof \ArrayObject) { + $value = $value->getArrayCopy(); + } + + if ($value instanceof \Traversable) { + $value = iterator_to_array($value); + } + + return (\is_array($value) && !array_is_list($value)) || \is_object($value); + } + + /** + * Renders a template. + * + * @param array $context + * @param string|array|TemplateWrapper $template The template to render or an array of templates to try consecutively + * @param array $variables The variables to pass to the template + * @param bool $withContext + * @param bool $ignoreMissing Whether to ignore missing templates or not + * @param bool $sandboxed Whether to sandbox the template or not + * + * @return string|Markup + * + * @internal + */ + public static function include(Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false) + { + $alreadySandboxed = false; + $sandbox = null; + if ($withContext) { + $variables = array_merge($context, $variables); + } + + if ($isSandboxed = $sandboxed && $env->hasExtension(SandboxExtension::class)) { + $sandbox = $env->getExtension(SandboxExtension::class); + if (!$alreadySandboxed = $sandbox->isSandboxed()) { + $sandbox->enableSandbox(); + } + } + + try { + $loaded = null; + try { + $loaded = $env->resolveTemplate($template); + } catch (LoaderError $e) { + if (!$ignoreMissing) { + throw $e; + } + + return ''; + } + + $rendered = $loaded->render($variables); + + return '' === $rendered ? '' : new Markup($rendered, $env->getCharset()); + } finally { + if ($isSandboxed && !$alreadySandboxed) { + $sandbox->disableSandbox(); + } + } + } + + /** + * Returns a template content without rendering it. + * + * @param string $name The template name + * @param bool $ignoreMissing Whether to ignore missing templates or not + * + * @internal + */ + public static function source(Environment $env, $name, $ignoreMissing = false): string + { + $loader = $env->getLoader(); + try { + return $loader->getSourceContext($name)->getCode(); + } catch (LoaderError $e) { + if (!$ignoreMissing) { + throw $e; + } + + return ''; + } + } + + /** + * Returns the list of cases of the enum. + * + * @template T of \UnitEnum + * + * @param class-string $enum + * + * @return list + * + * @internal + */ + public static function enumCases(string $enum): array + { + if (!enum_exists($enum)) { + throw new RuntimeError(\sprintf('Enum "%s" does not exist.', $enum)); + } + + return $enum::cases(); + } + + /** + * Provides the ability to access enums by their class names. + * + * @template T of \UnitEnum + * + * @param class-string $enum + * + * @return T + * + * @internal + */ + public static function enum(string $enum): \UnitEnum + { + if (!enum_exists($enum)) { + throw new RuntimeError(\sprintf('"%s" is not an enum.', $enum)); + } + + if (!$cases = $enum::cases()) { + throw new RuntimeError(\sprintf('"%s" is an empty enum.', $enum)); + } + + return $cases[0]; + } + + /** + * Provides the ability to get constants from instances as well as class/global constants. + * + * @param string $constant The name of the constant + * @param object|null $object The object to get the constant from + * @param bool $checkDefined Whether to check if the constant is defined or not + * + * @return mixed Class constants can return many types like scalars, arrays, and + * objects depending on the PHP version (\BackedEnum, \UnitEnum, etc.) + * When $checkDefined is true, returns true when the constant is defined, false otherwise + * + * @internal + */ + public static function constant($constant, $object = null, bool $checkDefined = false) + { + if (null !== $object) { + if ('class' === $constant) { + return $checkDefined ? true : $object::class; + } + + $constant = $object::class.'::'.$constant; + } + + if (!\defined($constant)) { + if ($checkDefined) { + return false; + } + + if ('::class' === strtolower(substr($constant, -7))) { + throw new RuntimeError(\sprintf('You cannot use the Twig function "constant" to access "%s". You could provide an object and call constant("class", $object) or use the class name directly as a string.', $constant)); + } + + throw new RuntimeError(\sprintf('Constant "%s" is undefined.', $constant)); + } + + return $checkDefined ? true : \constant($constant); + } + + /** + * Batches item. + * + * @param array $items An array of items + * @param int $size The size of the batch + * @param mixed $fill A value used to fill missing items + * + * @internal + */ + public static function batch($items, $size, $fill = null, $preserveKeys = true): array + { + if (!is_iterable($items)) { + throw new RuntimeError(\sprintf('The "batch" filter expects a sequence or a mapping, got "%s".', get_debug_type($items))); + } + + $size = (int) ceil($size); + + $result = array_chunk(self::toArray($items, $preserveKeys), $size, $preserveKeys); + + if (null !== $fill && $result) { + $last = \count($result) - 1; + if ($fillCount = $size - \count($result[$last])) { + for ($i = 0; $i < $fillCount; ++$i) { + $result[$last][] = $fill; + } + } + } + + return $result; + } + + /** + * Returns the attribute value for a given array/object. + * + * @param mixed $object The object or array from where to get the item + * @param mixed $item The item to get from the array or object + * @param array $arguments An array of arguments to pass if the item is an object method + * @param string $type The type of attribute (@see \Twig\Template constants) + * @param bool $isDefinedTest Whether this is only a defined check + * @param bool $ignoreStrictCheck Whether to ignore the strict attribute check or not + * @param int $lineno The template line where the attribute was called + * + * @return mixed The attribute value, or a Boolean when $isDefinedTest is true, or null when the attribute is not set and $ignoreStrictCheck is true + * + * @throws RuntimeError if the attribute does not exist and Twig is running in strict mode and $isDefinedTest is false + * + * @internal + */ + public static function getAttribute(Environment $env, Source $source, $object, $item, array $arguments = [], $type = Template::ANY_CALL, $isDefinedTest = false, $ignoreStrictCheck = false, $sandboxed = false, int $lineno = -1) + { + $propertyNotAllowedError = null; + if ($sandboxed && $item instanceof \Stringable) { + $env->getExtension(SandboxExtension::class)->ensureToStringAllowed($item, $lineno, $source); + } + + // array + if (Template::METHOD_CALL !== $type) { + $arrayItem = \is_bool($item) || \is_float($item) ? (int) $item : $item; + + if ($sandboxed && $object instanceof \ArrayAccess && !\in_array($object::class, self::ARRAY_LIKE_CLASSES, true)) { + try { + $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $arrayItem, $lineno, $source); + } catch (SecurityNotAllowedPropertyError $propertyNotAllowedError) { + // The methodCheck path expects $item to be a string; stringify it here + // to avoid PHP 8.1+ implicit float-to-int deprecations on downstream + // array key lookups (e.g. isset($cache[$class][$item])). + $item = (string) $item; + goto methodCheck; + } + } + + if (match (true) { + \is_array($object) => \array_key_exists($arrayItem = (string) $arrayItem, $object), + $object instanceof \ArrayAccess => $object->offsetExists($arrayItem), + default => false, + }) { + if ($isDefinedTest) { + return true; + } + + return $object[$arrayItem]; + } + + if (Template::ARRAY_CALL === $type || !\is_object($object)) { + if ($isDefinedTest) { + return false; + } + + if ($ignoreStrictCheck || !$env->isStrictVariables()) { + return; + } + + if ($object instanceof \ArrayAccess) { + if (\is_object($arrayItem) || \is_array($arrayItem)) { + $message = \sprintf('Key of type "%s" does not exist in ArrayAccess-able object of class "%s".', get_debug_type($arrayItem), get_debug_type($object)); + } else { + $message = \sprintf('Key "%s" does not exist in ArrayAccess-able object of class "%s".', $arrayItem, get_debug_type($object)); + } + } elseif (\is_object($object)) { + $message = \sprintf('Impossible to access a key "%s" on an object of class "%s" that does not implement ArrayAccess interface.', $item, get_debug_type($object)); + } elseif (\is_array($object)) { + if (!$object) { + $message = \sprintf('Key "%s" does not exist as the sequence/mapping is empty.', $arrayItem); + } else { + $message = \sprintf('Key "%s" for sequence/mapping with keys "%s" does not exist.', $arrayItem, implode(', ', array_keys($object))); + } + } elseif (Template::ARRAY_CALL === $type) { + if (null === $object) { + $message = \sprintf('Impossible to access a key ("%s") on a null variable.', $item); + } else { + $message = \sprintf('Impossible to access a key ("%s") on a %s variable ("%s").', $item, get_debug_type($object), $object); + } + } elseif (null === $object) { + $message = \sprintf('Impossible to access an attribute ("%s") on a null variable.', $item); + } else { + $message = \sprintf('Impossible to access an attribute ("%s") on a %s variable ("%s").', $item, get_debug_type($object), $object); + } + + throw new RuntimeError($message, $lineno, $source); + } + } + + $item = (string) $item; + + if (!\is_object($object)) { + if ($isDefinedTest) { + return false; + } + + if ($ignoreStrictCheck || !$env->isStrictVariables()) { + return; + } + + if (null === $object) { + $message = \sprintf('Impossible to invoke a method ("%s") on a null variable.', $item); + } elseif (\is_array($object)) { + $message = \sprintf('Impossible to invoke a method ("%s") on a sequence/mapping.', $item); + } else { + $message = \sprintf('Impossible to invoke a method ("%s") on a %s variable ("%s").', $item, get_debug_type($object), $object); + } + + throw new RuntimeError($message, $lineno, $source); + } + + if ($object instanceof Template) { + throw new RuntimeError('Accessing \Twig\Template attributes is forbidden.', $lineno, $source); + } + + // object property + if (Template::METHOD_CALL !== $type) { + if ($sandboxed) { + try { + $env->getExtension(SandboxExtension::class)->checkPropertyAllowed($object, $item, $lineno, $source); + } catch (SecurityNotAllowedPropertyError $propertyNotAllowedError) { + goto methodCheck; + } + } + + static $propertyCheckers = []; + + if (isset($object->$item) + || ($propertyCheckers[$object::class][$item] ??= self::getPropertyChecker($object::class, $item))($object, $item) + ) { + if ($isDefinedTest) { + return true; + } + + return $object->$item; + } + + if ($object instanceof \DateTimeInterface && \in_array($item, ['date', 'timezone', 'timezone_type'], true)) { + if ($isDefinedTest) { + return true; + } + + return ((array) $object)[$item]; + } + + if (\defined($object::class.'::'.$item)) { + if ($isDefinedTest) { + return true; + } + + return \constant($object::class.'::'.$item); + } + } + + methodCheck: + + static $cache = []; + + $class = $object::class; + + // object method + // precedence: getXxx() > isXxx() > hasXxx() + if (!isset($cache[$class])) { + $methods = get_class_methods($object); + if ($object instanceof \Closure) { + $methods[] = '__invoke'; + } + sort($methods); + $lcMethods = array_map('strtolower', $methods); + $classCache = []; + foreach ($methods as $i => $method) { + $classCache[$method] = $method; + $classCache[$lcName = $lcMethods[$i]] = $method; + + if ('g' === $lcName[0] && str_starts_with($lcName, 'get')) { + $prefixLength = 3; + $lcName = substr($lcName, $prefixLength); + } elseif ('i' === $lcName[0] && str_starts_with($lcName, 'is')) { + $prefixLength = 2; + $lcName = substr($lcName, $prefixLength); + } elseif ('h' === $lcName[0] && str_starts_with($lcName, 'has')) { + $prefixLength = 3; + $lcName = substr($lcName, $prefixLength); + if (\in_array('is'.$lcName, $lcMethods, true)) { + continue; + } + } else { + continue; + } + + // skip get(), is() and has() methods (in which case, $lcName is empty) + if ($lcName) { + // camelCase name (e.g. getFooBar() -> fooBar) + $name = $lcName[0].substr($method, $prefixLength + 1); + + if (!isset($classCache[$name])) { + $classCache[$name] = $method; + } + + if (!isset($classCache[$lcName])) { + $classCache[$lcName] = $method; + } + } + } + $cache[$class] = $classCache; + } + + $call = false; + if (isset($cache[$class][$item])) { + $method = $cache[$class][$item]; + } elseif (isset($cache[$class][$lcItem = strtolower($item)])) { + $method = $cache[$class][$lcItem]; + } elseif (isset($cache[$class]['__call'])) { + $method = $item; + $call = true; + } else { + if ($isDefinedTest) { + return false; + } + + if ($propertyNotAllowedError) { + throw $propertyNotAllowedError; + } + + if ($ignoreStrictCheck || !$env->isStrictVariables()) { + return; + } + + throw new RuntimeError(\sprintf('Neither the property "%1$s" nor one of the methods "%1$s()", "get%1$s()", "is%1$s()", "has%1$s()" or "__call()" exist and have public access in class "%2$s".', $item, $class), $lineno, $source); + } + + if ($sandboxed) { + try { + $env->getExtension(SandboxExtension::class)->checkMethodAllowed($object, $method, $lineno, $source); + } catch (SecurityNotAllowedMethodError $e) { + if ($isDefinedTest) { + return false; + } + + if ($propertyNotAllowedError) { + throw $propertyNotAllowedError; + } + + throw $e; + } + } + + if ($isDefinedTest) { + return true; + } + + // Some objects throw exceptions when they have __call, and the method we try + // to call is not supported. If ignoreStrictCheck is true, we should return null. + try { + $ret = $object->$method(...$arguments); + } catch (\BadMethodCallException $e) { + if ($call && ($ignoreStrictCheck || !$env->isStrictVariables())) { + return; + } + throw $e; + } + + return $ret; + } + + /** + * Returns the values from a single column in the input array. + * + *
+     *  {% set items = [{ 'fruit' : 'apple'}, {'fruit' : 'orange' }] %}
+     *
+     *  {% set fruits = items|column('fruit') %}
+     *
+     *  {# fruits now contains ['apple', 'orange'] #}
+     * 
+ * + * @param array|\Traversable $array An array + * @param int|string $name The column name + * @param int|string|null $index The column to use as the index/keys for the returned array + * + * @return array The array of values + * + * @internal + */ + public static function column(Environment $env, bool $isSandboxed, $array, $name, $index = null): array + { + if (!is_iterable($array)) { + throw new RuntimeError(\sprintf('The "column" filter expects a sequence or a mapping, got "%s".', get_debug_type($array))); + } + + if ($array instanceof \Traversable) { + $array = iterator_to_array($array); + } + + if ($isSandboxed) { + // The sandbox might be enabled via a SourcePolicyInterface, in which case the SandboxExtension + // would not consider the sandbox active without the current Source: $isSandboxed is already + // computed against the call-site source, so check the policy directly to honor that decision. + $policy = $env->getExtension(SandboxExtension::class)->getSecurityPolicy(); + foreach ($array as $item) { + if (\is_object($item)) { + $policy->checkPropertyAllowed($item, (string) $name); + if (null !== $index) { + $policy->checkPropertyAllowed($item, (string) $index); + } + } + } + } + + return array_column($array, $name, $index); + } + + /** + * @param \Closure $arrow + * + * @internal + */ + public static function filter(Environment $env, bool $isSandboxed, $array, $arrow) + { + if (!is_iterable($array)) { + throw new RuntimeError(\sprintf('The "filter" filter expects a sequence/mapping or "Traversable", got "%s".', get_debug_type($array))); + } + + self::checkArrow($isSandboxed, $arrow, 'filter', 'filter'); + + if (\is_array($array)) { + return array_filter($array, $arrow, \ARRAY_FILTER_USE_BOTH); + } + + // the IteratorIterator wrapping is needed as some internal PHP classes are \Traversable but do not implement \Iterator + return new \CallbackFilterIterator(new \IteratorIterator($array), $arrow); + } + + /** + * @param \Closure $arrow + * + * @internal + */ + public static function find(Environment $env, bool $isSandboxed, $array, $arrow) + { + if (!is_iterable($array)) { + throw new RuntimeError(\sprintf('The "find" filter expects a sequence or a mapping, got "%s".', get_debug_type($array))); + } + + self::checkArrow($isSandboxed, $arrow, 'find', 'filter'); + + foreach ($array as $k => $v) { + if ($arrow($v, $k)) { + return $v; + } + } + + return null; + } + + /** + * @param \Closure $arrow + * + * @internal + */ + public static function map(Environment $env, bool $isSandboxed, $array, $arrow) + { + if (!is_iterable($array)) { + throw new RuntimeError(\sprintf('The "map" filter expects a sequence or a mapping, got "%s".', get_debug_type($array))); + } + + self::checkArrow($isSandboxed, $arrow, 'map', 'filter'); + + $r = []; + foreach ($array as $k => $v) { + $r[$k] = $arrow($v, $k); + } + + return $r; + } + + /** + * @param \Closure $arrow + * + * @internal + */ + public static function reduce(Environment $env, bool $isSandboxed, $array, $arrow, $initial = null) + { + if (!is_iterable($array)) { + throw new RuntimeError(\sprintf('The "reduce" filter expects a sequence or a mapping, got "%s".', get_debug_type($array))); + } + + self::checkArrow($isSandboxed, $arrow, 'reduce', 'filter'); + + $accumulator = $initial; + foreach ($array as $key => $value) { + $accumulator = $arrow($accumulator, $value, $key); + } + + return $accumulator; + } + + /** + * @param \Closure $arrow + * + * @internal + */ + public static function arraySome(Environment $env, $array, $arrow, bool $isSandboxed = false) + { + if (!is_iterable($array)) { + throw new RuntimeError(\sprintf('The "has some" test expects a sequence or a mapping, got "%s".', get_debug_type($array))); + } + + self::checkArrow($isSandboxed, $arrow, 'has some', 'operator'); + + foreach ($array as $k => $v) { + if ($arrow($v, $k)) { + return true; + } + } + + return false; + } + + /** + * @param \Closure $arrow + * + * @internal + */ + public static function arrayEvery(Environment $env, $array, $arrow, bool $isSandboxed = false) + { + if (!is_iterable($array)) { + throw new RuntimeError(\sprintf('The "has every" test expects a sequence or a mapping, got "%s".', get_debug_type($array))); + } + + self::checkArrow($isSandboxed, $arrow, 'has every', 'operator'); + + foreach ($array as $k => $v) { + if (!$arrow($v, $k)) { + return false; + } + } + + return true; + } + + /** + * @internal + */ + public static function checkArrow(bool $isSandboxed, $arrow, $thing, $type) + { + if ($arrow instanceof \Closure) { + return; + } + + if ($isSandboxed) { + throw new RuntimeError(\sprintf('The callable passed to the "%s" %s must be a Closure in sandbox mode.', $thing, $type)); + } + + trigger_deprecation('twig/twig', '3.15', 'Passing a callable that is not a PHP \Closure as an argument to the "%s" %s is deprecated.', $thing, $type); + } + + /** + * @internal to be removed in Twig 4 + */ + public static function captureOutput(iterable $body): string + { + $level = ob_get_level(); + ob_start(); + + try { + foreach ($body as $data) { + echo $data; + } + } catch (\Throwable $e) { + while (ob_get_level() > $level) { + ob_end_clean(); + } + + throw $e; + } + + return ob_get_clean(); + } + + /** + * @internal + */ + public static function parseParentFunction(Parser $parser, Node $fakeNode, $args, int $line): AbstractExpression + { + if (!$blockName = $parser->peekBlockStack()) { + throw new SyntaxError('Calling the "parent" function outside of a block is forbidden.', $line, $parser->getStream()->getSourceContext()); + } + + if (!$parser->hasInheritance()) { + throw new SyntaxError('Calling the "parent" function on a template that does not call "extends" or "use" is forbidden.', $line, $parser->getStream()->getSourceContext()); + } + + return new ParentExpression($blockName, $line); + } + + /** + * @internal + */ + public static function parseBlockFunction(Parser $parser, Node $fakeNode, $args, int $line): AbstractExpression + { + $fakeFunction = new TwigFunction('block', static fn ($name, $template = null) => null); + $args = (new CallableArgumentsExtractor($fakeNode, $fakeFunction))->extractArguments($args); + + return new BlockReferenceExpression($args[0], $args[1] ?? null, $line); + } + + /** + * @internal + */ + public static function parseAttributeFunction(Parser $parser, Node $fakeNode, $args, int $line): AbstractExpression + { + $fakeFunction = new TwigFunction('attribute', static fn ($variable, $attribute, $arguments = null) => null); + $args = (new CallableArgumentsExtractor($fakeNode, $fakeFunction))->extractArguments($args); + + /* + Deprecation to uncomment sometimes during the lifetime of the 4.x branch + $src = $parser->getStream()->getSourceContext(); + $dep = new DeprecatedCallableInfo('twig/twig', '3.15', 'The "attribute" function is deprecated, use the "." notation instead.'); + $dep->setName('attribute'); + $dep->setType('function'); + $dep->triggerDeprecation($src->getPath() ?: $src->getName(), $line); + */ + + return new GetAttrExpression($args[0], $args[1], $args[2] ?? null, Template::ANY_CALL, $line); + } + + private static function getPropertyChecker(string $class, string $property): \Closure + { + static $classReflectors = []; + + $class = $classReflectors[$class] ??= new \ReflectionClass($class); + + if (!$class->hasProperty($property)) { + static $propertyExists; + + return $propertyExists ??= \Closure::fromCallable('property_exists'); + } + + $property = $class->getProperty($property); + + if (!$property->isPublic() || $property->isStatic()) { + static $false; + + return $false ??= static fn () => false; + } + + return static fn ($object) => $property->isInitialized($object); + } +} diff --git a/novaconium/vendor/twig/src/Extension/DebugExtension.php b/novaconium/vendor/twig/src/Extension/DebugExtension.php new file mode 100644 index 0000000..dac21c3 --- /dev/null +++ b/novaconium/vendor/twig/src/Extension/DebugExtension.php @@ -0,0 +1,62 @@ + $isDumpOutputHtmlSafe ? ['html'] : [], 'needs_context' => true, 'needs_environment' => true, 'is_variadic' => true]), + ]; + } + + /** + * @internal + */ + public static function dump(Environment $env, $context, ...$vars) + { + if (!$env->isDebug()) { + return; + } + + ob_start(); + + if (!$vars) { + $vars = []; + foreach ($context as $key => $value) { + if (!$value instanceof Template && !$value instanceof TemplateWrapper) { + $vars[$key] = $value; + } + } + + var_dump($vars); + } else { + var_dump(...$vars); + } + + return ob_get_clean(); + } +} diff --git a/novaconium/vendor/twig/src/Extension/EscaperExtension.php b/novaconium/vendor/twig/src/Extension/EscaperExtension.php new file mode 100644 index 0000000..6d9d1ce --- /dev/null +++ b/novaconium/vendor/twig/src/Extension/EscaperExtension.php @@ -0,0 +1,221 @@ +setDefaultStrategy($defaultStrategy); + } + + public function getTokenParsers(): array + { + return [new AutoEscapeTokenParser()]; + } + + public function getNodeVisitors(): array + { + return [new EscaperNodeVisitor()]; + } + + public function getFilters(): array + { + return [ + new TwigFilter('escape', [EscaperRuntime::class, 'escape'], ['is_safe_callback' => [self::class, 'escapeFilterIsSafe']]), + new TwigFilter('e', [EscaperRuntime::class, 'escape'], ['is_safe_callback' => [self::class, 'escapeFilterIsSafe']]), + new TwigFilter('raw', null, ['is_safe' => ['all'], 'node_class' => RawFilter::class]), + ]; + } + + public function getLastModified(): int + { + return max( + parent::getLastModified(), + filemtime((new \ReflectionClass(EscaperRuntime::class))->getFileName()), + ); + } + + /** + * @deprecated since Twig 3.10 + */ + public function setEnvironment(Environment $environment): void + { + $triggerDeprecation = \func_num_args() > 1 ? func_get_arg(1) : true; + if ($triggerDeprecation) { + trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated and not needed if you are using methods from "Twig\Runtime\EscaperRuntime".', __METHOD__); + } + + $this->environment = $environment; + $this->escaper = null; + } + + /** + * @return void + * + * @deprecated since Twig 3.10 + */ + public function setEscaperRuntime(EscaperRuntime $escaper) + { + trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated and not needed if you are using methods from "Twig\Runtime\EscaperRuntime".', __METHOD__); + + $this->escaper = $escaper; + } + + /** + * Sets the default strategy to use when not defined by the user. + * + * The strategy can be a valid PHP callback that takes the template + * name as an argument and returns the strategy to use. + * + * @param string|false|callable(string $templateName): string $defaultStrategy An escaping strategy + */ + public function setDefaultStrategy($defaultStrategy): void + { + if ('name' === $defaultStrategy) { + $defaultStrategy = [FileExtensionEscapingStrategy::class, 'guess']; + } + + $this->defaultStrategy = $defaultStrategy; + } + + /** + * Gets the default strategy to use when not defined by the user. + * + * @param string $name The template name + * + * @return string|false The default strategy to use for the template + */ + public function getDefaultStrategy(string $name) + { + // disable string callables to avoid calling a function named html or js, + // or any other upcoming escaping strategy + if (!\is_string($this->defaultStrategy) && false !== $this->defaultStrategy) { + return \call_user_func($this->defaultStrategy, $name); + } + + return $this->defaultStrategy; + } + + /** + * Defines a new escaper to be used via the escape filter. + * + * @param string $strategy The strategy name that should be used as a strategy in the escape call + * @param callable(Environment, string, string): string $callable A valid PHP callable + * + * @return void + * + * @deprecated since Twig 3.10 + */ + public function setEscaper($strategy, callable $callable) + { + trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::setEscaper()" method instead (be warned that Environment is not passed anymore to the callable).', __METHOD__); + + $escaper = $this->getEscaper(__METHOD__); + + $this->escapers[$strategy] = $callable; + $callable = function ($string, $charset) use ($callable) { + return $callable($this->environment, $string, $charset); + }; + + $escaper->setEscaper($strategy, $callable); + } + + /** + * Gets all defined escapers. + * + * @return array An array of escapers + * + * @deprecated since Twig 3.10 + */ + public function getEscapers() + { + trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::getEscaper()" method instead.', __METHOD__); + + return $this->escapers; + } + + /** + * @return void + * + * @deprecated since Twig 3.10 + */ + public function setSafeClasses(array $safeClasses = []) + { + trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::setSafeClasses()" method instead.', __METHOD__); + + $this->getEscaper(__METHOD__)->setSafeClasses($safeClasses); + } + + /** + * @return void + * + * @deprecated since Twig 3.10 + */ + public function addSafeClass(string $class, array $strategies) + { + trigger_deprecation('twig/twig', '3.10', 'The "%s()" method is deprecated, use the "Twig\Runtime\EscaperRuntime::addSafeClass()" method instead.', __METHOD__); + + $this->getEscaper(__METHOD__)->addSafeClass($class, $strategies); + } + + private function getEscaper(string $fromMethod): EscaperRuntime + { + if (isset($this->escaper)) { + return $this->escaper; + } + + if (isset($this->environment)) { + return $this->escaper = $this->environment->getRuntime(EscaperRuntime::class); + } + + throw new \LogicException(\sprintf('You must call "setEnvironment()" before calling "%s()".', $fromMethod)); + } + + /** + * @internal + * + * @return array + */ + public static function escapeFilterIsSafe(Node $filterArgs) + { + foreach ($filterArgs as $arg) { + if ($arg instanceof ConstantExpression) { + return [$arg->getAttribute('value')]; + } + + return []; + } + + return ['html']; + } +} diff --git a/novaconium/vendor/twig/src/Extension/ExtensionInterface.php b/novaconium/vendor/twig/src/Extension/ExtensionInterface.php new file mode 100644 index 0000000..60ca35b --- /dev/null +++ b/novaconium/vendor/twig/src/Extension/ExtensionInterface.php @@ -0,0 +1,80 @@ + + * + * @method array getExpressionParsers() + */ +interface ExtensionInterface +{ + /** + * Returns the token parser instances to add to the existing list. + * + * @return TokenParserInterface[] + */ + public function getTokenParsers(); + + /** + * Returns the node visitor instances to add to the existing list. + * + * @return NodeVisitorInterface[] + */ + public function getNodeVisitors(); + + /** + * Returns a list of filters to add to the existing list. + * + * @return TwigFilter[] + */ + public function getFilters(); + + /** + * Returns a list of tests to add to the existing list. + * + * @return TwigTest[] + */ + public function getTests(); + + /** + * Returns a list of functions to add to the existing list. + * + * @return TwigFunction[] + */ + public function getFunctions(); + + /** + * Returns a list of operators to add to the existing list. + * + * @return array + * + * @psalm-return array{ + * array}>, + * array, associativity: ExpressionParser::OPERATOR_*}> + * } + */ + public function getOperators(); +} diff --git a/novaconium/vendor/twig/src/Extension/GlobalsInterface.php b/novaconium/vendor/twig/src/Extension/GlobalsInterface.php new file mode 100644 index 0000000..d52cd10 --- /dev/null +++ b/novaconium/vendor/twig/src/Extension/GlobalsInterface.php @@ -0,0 +1,25 @@ + + */ +interface GlobalsInterface +{ + /** + * @return array + */ + public function getGlobals(): array; +} diff --git a/novaconium/vendor/twig/src/Extension/LastModifiedExtensionInterface.php b/novaconium/vendor/twig/src/Extension/LastModifiedExtensionInterface.php new file mode 100644 index 0000000..4bab0c0 --- /dev/null +++ b/novaconium/vendor/twig/src/Extension/LastModifiedExtensionInterface.php @@ -0,0 +1,23 @@ +optimizers)]; + } +} diff --git a/novaconium/vendor/twig/src/Extension/ProfilerExtension.php b/novaconium/vendor/twig/src/Extension/ProfilerExtension.php new file mode 100644 index 0000000..43e4a44 --- /dev/null +++ b/novaconium/vendor/twig/src/Extension/ProfilerExtension.php @@ -0,0 +1,52 @@ +actives[] = $profile; + } + + /** + * @return void + */ + public function enter(Profile $profile) + { + $this->actives[0]->addProfile($profile); + array_unshift($this->actives, $profile); + } + + /** + * @return void + */ + public function leave(Profile $profile) + { + $profile->leave(); + array_shift($this->actives); + + if (1 === \count($this->actives)) { + $this->actives[0]->leave(); + } + } + + public function getNodeVisitors(): array + { + return [new ProfilerNodeVisitor(static::class)]; + } +} diff --git a/novaconium/vendor/twig/src/Extension/RuntimeExtensionInterface.php b/novaconium/vendor/twig/src/Extension/RuntimeExtensionInterface.php new file mode 100644 index 0000000..63bc3b1 --- /dev/null +++ b/novaconium/vendor/twig/src/Extension/RuntimeExtensionInterface.php @@ -0,0 +1,19 @@ + + */ +interface RuntimeExtensionInterface +{ +} diff --git a/novaconium/vendor/twig/src/Extension/SandboxExtension.php b/novaconium/vendor/twig/src/Extension/SandboxExtension.php new file mode 100644 index 0000000..181e58c --- /dev/null +++ b/novaconium/vendor/twig/src/Extension/SandboxExtension.php @@ -0,0 +1,247 @@ +policy = $policy; + $this->sandboxedGlobally = $sandboxed; + $this->sourcePolicy = $sourcePolicy; + } + + public function getTokenParsers(): array + { + return [new SandboxTokenParser()]; + } + + public function getNodeVisitors(): array + { + return [new SandboxNodeVisitor()]; + } + + public function enableSandbox(): void + { + $this->sandboxed = true; + } + + public function disableSandbox(): void + { + $this->sandboxed = false; + } + + public function isSandboxed(?Source $source = null): bool + { + return $this->sandboxedGlobally || $this->sandboxed || $this->isSourceSandboxed($source); + } + + public function isSandboxedGlobally(): bool + { + return $this->sandboxedGlobally; + } + + private function isSourceSandboxed(?Source $source): bool + { + if (null === $source || null === $this->sourcePolicy) { + return false; + } + + return $this->sourcePolicy->enableSandbox($source); + } + + public function setSecurityPolicy(SecurityPolicyInterface $policy): void + { + $this->policy = $policy; + } + + public function getSecurityPolicy(): SecurityPolicyInterface + { + return $this->policy; + } + + public function checkSecurity($tags, $filters, $functions, $tests = [], $source = null): void + { + // BC: previous signature was checkSecurity($tags, $filters, $functions, ?Source $source = null); + // detect a legacy call where the 4th positional argument was the Source. + if ($tests instanceof Source || (null === $tests && \func_num_args() < 5)) { + trigger_deprecation('twig/twig', '3.28', 'Passing a "Twig\Source" as the 4th argument of "%s()" is deprecated; pass an array of tests instead.', __METHOD__); + $source = $tests; + $tests = []; + } + + if (!$this->isSandboxed($source)) { + return; + } + + if ((new \ReflectionMethod($this->policy, 'checkSecurity'))->getNumberOfParameters() >= 4) { + $this->policy->checkSecurity($tags, $filters, $functions, $tests); + + return; + } + + trigger_deprecation('twig/twig', '3.28', 'The "%s::checkSecurity()" method will take a 4th "array $tests" argument in 4.0; not declaring it is deprecated.', $this->policy::class); + + $this->policy->checkSecurity($tags, $filters, $functions); + } + + public function checkMethodAllowed($obj, $method, int $lineno = -1, ?Source $source = null): void + { + if ($this->isSandboxed($source)) { + try { + $this->policy->checkMethodAllowed($obj, $method); + } catch (SecurityNotAllowedMethodError $e) { + $e->setSourceContext($source); + $e->setTemplateLine($lineno); + + throw $e; + } + } + } + + public function checkPropertyAllowed($obj, $property, int $lineno = -1, ?Source $source = null): void + { + if ($this->isSandboxed($source)) { + try { + $this->policy->checkPropertyAllowed($obj, $property); + } catch (SecurityNotAllowedPropertyError $e) { + $e->setSourceContext($source); + $e->setTemplateLine($lineno); + + throw $e; + } + } + } + + /** + * @throws SecurityNotAllowedMethodError + */ + public function ensureToStringAllowed($obj, int $lineno = -1, ?Source $source = null) + { + return $this->doEnsureToStringAllowed($obj, $lineno, $source, new \SplObjectStorage()); + } + + /** + * Materialises a spread operand and runs the policy on every element. + * + * @internal + * + * @throws SecurityNotAllowedMethodError + */ + public function ensureSpreadAllowed(iterable $obj, int $lineno = -1, ?Source $source = null): array + { + $seen = new \SplObjectStorage(); + if ($obj instanceof \Traversable) { + $seen[$obj] = true; + $obj = iterator_to_array($obj); + } + + $this->ensureToStringAllowedForArray($obj, $lineno, $source, $seen); + + return $obj; + } + + private function doEnsureToStringAllowed($obj, int $lineno, ?Source $source, \SplObjectStorage $seen) + { + if (\is_array($obj)) { + $this->ensureToStringAllowedForArray($obj, $lineno, $source, $seen); + + return $obj; + } + + if (!$this->isSandboxed($source)) { + return $obj; + } + + if ($obj instanceof \Stringable) { + try { + $this->policy->checkMethodAllowed($obj, '__toString'); + } catch (SecurityNotAllowedMethodError $e) { + $e->setSourceContext($source); + $e->setTemplateLine($lineno); + + throw $e; + } + } + + // Elements yielded by a Traversable may be string-coerced downstream + // (e.g. by `join`/`replace`), bypassing the policy. Check them now. + if ($obj instanceof \Traversable) { + if (isset($seen[$obj])) { + return $obj; + } + $seen[$obj] = true; + + // IteratorAggregate::getIterator() is idempotent, so we can walk + // the elements and return the original object: host code typed + // against a specific class (e.g. FormView) keeps working. + if ($obj instanceof \IteratorAggregate) { + foreach ($obj as $v) { + $this->doEnsureToStringAllowed($v, $lineno, $source, $seen); + } + + return $obj; + } + + // Single-pass Iterator/Generator: materialise to validate. + $array = iterator_to_array($obj); + $this->ensureToStringAllowedForArray($array, $lineno, $source, $seen); + + if (!$obj instanceof \Stringable) { + return $array; + } + } + + return $obj; + } + + private function ensureToStringAllowedForArray(array $obj, int $lineno, ?Source $source, \SplObjectStorage $seen, array &$stack = []): void + { + foreach ($obj as $k => $v) { + if (!$v) { + continue; + } + + if (!\is_array($v)) { + $this->doEnsureToStringAllowed($v, $lineno, $source, $seen); + continue; + } + + if ($r = \ReflectionReference::fromArrayElement($obj, $k)) { + if (isset($stack[$r->getId()])) { + continue; + } + + $stack[$r->getId()] = true; + } + + $this->ensureToStringAllowedForArray($v, $lineno, $source, $seen, $stack); + } + } +} diff --git a/novaconium/vendor/twig/src/Extension/StagingExtension.php b/novaconium/vendor/twig/src/Extension/StagingExtension.php new file mode 100644 index 0000000..59db2ca --- /dev/null +++ b/novaconium/vendor/twig/src/Extension/StagingExtension.php @@ -0,0 +1,100 @@ + + * + * @internal + */ +final class StagingExtension extends AbstractExtension +{ + private $functions = []; + private $filters = []; + private $visitors = []; + private $tokenParsers = []; + private $tests = []; + + public function addFunction(TwigFunction $function): void + { + if (isset($this->functions[$function->getName()])) { + throw new \LogicException(\sprintf('Function "%s" is already registered.', $function->getName())); + } + + $this->functions[$function->getName()] = $function; + } + + public function getFunctions(): array + { + return $this->functions; + } + + public function addFilter(TwigFilter $filter): void + { + if (isset($this->filters[$filter->getName()])) { + throw new \LogicException(\sprintf('Filter "%s" is already registered.', $filter->getName())); + } + + $this->filters[$filter->getName()] = $filter; + } + + public function getFilters(): array + { + return $this->filters; + } + + public function addNodeVisitor(NodeVisitorInterface $visitor): void + { + $this->visitors[] = $visitor; + } + + public function getNodeVisitors(): array + { + return $this->visitors; + } + + public function addTokenParser(TokenParserInterface $parser): void + { + if (isset($this->tokenParsers[$parser->getTag()])) { + throw new \LogicException(\sprintf('Tag "%s" is already registered.', $parser->getTag())); + } + + $this->tokenParsers[$parser->getTag()] = $parser; + } + + public function getTokenParsers(): array + { + return $this->tokenParsers; + } + + public function addTest(TwigTest $test): void + { + if (isset($this->tests[$test->getName()])) { + throw new \LogicException(\sprintf('Test "%s" is already registered.', $test->getName())); + } + + $this->tests[$test->getName()] = $test; + } + + public function getTests(): array + { + return $this->tests; + } +} diff --git a/novaconium/vendor/twig/src/Extension/StringLoaderExtension.php b/novaconium/vendor/twig/src/Extension/StringLoaderExtension.php new file mode 100644 index 0000000..da5b2da --- /dev/null +++ b/novaconium/vendor/twig/src/Extension/StringLoaderExtension.php @@ -0,0 +1,43 @@ + true]), + ]; + } + + /** + * Loads a template from a string. + * + * {{ include(template_from_string("Hello {{ name }}")) }} + * + * Never expose `template_from_string` to untrusted template + * authors (like in a sandboxed environment). See the docs for more details. + * + * @param string|null $name An optional name of the template to be used in error messages + * + * @internal + */ + public static function templateFromString(Environment $env, string|\Stringable $template, ?string $name = null): TemplateWrapper + { + return $env->createTemplate((string) $template, $name); + } +} diff --git a/novaconium/vendor/twig/src/Extension/YieldNotReadyExtension.php b/novaconium/vendor/twig/src/Extension/YieldNotReadyExtension.php new file mode 100644 index 0000000..49dfb80 --- /dev/null +++ b/novaconium/vendor/twig/src/Extension/YieldNotReadyExtension.php @@ -0,0 +1,30 @@ +useYield)]; + } +} diff --git a/novaconium/vendor/twig/src/ExtensionSet.php b/novaconium/vendor/twig/src/ExtensionSet.php new file mode 100644 index 0000000..7bb62fa --- /dev/null +++ b/novaconium/vendor/twig/src/ExtensionSet.php @@ -0,0 +1,567 @@ + + * + * @internal + */ +final class ExtensionSet +{ + private $extensions; + private $initialized = false; + private $runtimeInitialized = false; + private $staging; + private $parsers; + private $visitors; + /** @var array */ + private $filters; + /** @var array */ + private $dynamicFilters; + /** @var array */ + private $tests; + /** @var array */ + private $dynamicTests; + /** @var array */ + private $functions; + /** @var array */ + private $dynamicFunctions; + private ExpressionParsers $expressionParsers; + /** @var array|null */ + private $globals; + /** @var array */ + private $functionCallbacks = []; + /** @var array */ + private $filterCallbacks = []; + /** @var array */ + private $testCallbacks = []; + /** @var array */ + private $parserCallbacks = []; + private $lastModified = 0; + + public function __construct() + { + $this->staging = new StagingExtension(); + } + + /** + * @return void + */ + public function initRuntime() + { + $this->runtimeInitialized = true; + } + + public function hasExtension(string $class): bool + { + return isset($this->extensions[ltrim($class, '\\')]); + } + + public function getExtension(string $class): ExtensionInterface + { + $class = ltrim($class, '\\'); + + if (!isset($this->extensions[$class])) { + throw new RuntimeError(\sprintf('The "%s" extension is not enabled.', $class)); + } + + return $this->extensions[$class]; + } + + /** + * @param ExtensionInterface[] $extensions + */ + public function setExtensions(array $extensions): void + { + foreach ($extensions as $extension) { + $this->addExtension($extension); + } + } + + /** + * @return ExtensionInterface[] + */ + public function getExtensions(): array + { + return $this->extensions; + } + + public function getSignature(): string + { + return json_encode(array_keys($this->extensions)); + } + + public function isInitialized(): bool + { + return $this->initialized || $this->runtimeInitialized; + } + + public function getLastModified(): int + { + if (0 !== $this->lastModified) { + return $this->lastModified; + } + + $lastModified = 0; + foreach ($this->extensions as $extension) { + if ($extension instanceof LastModifiedExtensionInterface) { + $lastModified = max($extension->getLastModified(), $lastModified); + } else { + $r = new \ReflectionObject($extension); + if (is_file($r->getFileName())) { + $lastModified = max(filemtime($r->getFileName()), $lastModified); + } + } + } + + return $this->lastModified = $lastModified; + } + + public function addExtension(ExtensionInterface $extension): void + { + if ($extension instanceof AttributeExtension) { + $class = $extension->getClass(); + } else { + $class = $extension::class; + } + + if ($this->initialized) { + throw new \LogicException(\sprintf('Unable to register extension "%s" as extensions have already been initialized.', $class)); + } + + if (isset($this->extensions[$class])) { + throw new \LogicException(\sprintf('Unable to register extension "%s" as it is already registered.', $class)); + } + + $this->extensions[$class] = $extension; + } + + public function addFunction(TwigFunction $function): void + { + if ($this->initialized) { + throw new \LogicException(\sprintf('Unable to add function "%s" as extensions have already been initialized.', $function->getName())); + } + + $this->staging->addFunction($function); + } + + /** + * @return TwigFunction[] + */ + public function getFunctions(): array + { + if (!$this->initialized) { + $this->initExtensions(); + } + + return $this->functions; + } + + public function getFunction(string $name): ?TwigFunction + { + if (!$this->initialized) { + $this->initExtensions(); + } + + if (isset($this->functions[$name])) { + return $this->functions[$name]; + } + + foreach ($this->dynamicFunctions as $pattern => $function) { + if (preg_match($pattern, $name, $matches)) { + array_shift($matches); + + return $function->withDynamicArguments($name, $function->getName(), $matches); + } + } + + foreach ($this->functionCallbacks as $callback) { + if (false !== $function = $callback($name)) { + return $function; + } + } + + return null; + } + + /** + * @param callable(string): (TwigFunction|false) $callable + */ + public function registerUndefinedFunctionCallback(callable $callable): void + { + $this->functionCallbacks[] = $callable; + } + + public function addFilter(TwigFilter $filter): void + { + if ($this->initialized) { + throw new \LogicException(\sprintf('Unable to add filter "%s" as extensions have already been initialized.', $filter->getName())); + } + + $this->staging->addFilter($filter); + } + + /** + * @return TwigFilter[] + */ + public function getFilters(): array + { + if (!$this->initialized) { + $this->initExtensions(); + } + + return $this->filters; + } + + public function getFilter(string $name): ?TwigFilter + { + if (!$this->initialized) { + $this->initExtensions(); + } + + if (isset($this->filters[$name])) { + return $this->filters[$name]; + } + + foreach ($this->dynamicFilters as $pattern => $filter) { + if (preg_match($pattern, $name, $matches)) { + array_shift($matches); + + return $filter->withDynamicArguments($name, $filter->getName(), $matches); + } + } + + foreach ($this->filterCallbacks as $callback) { + if (false !== $filter = $callback($name)) { + return $filter; + } + } + + return null; + } + + /** + * @param callable(string): (TwigFilter|false) $callable + */ + public function registerUndefinedFilterCallback(callable $callable): void + { + $this->filterCallbacks[] = $callable; + } + + public function addNodeVisitor(NodeVisitorInterface $visitor): void + { + if ($this->initialized) { + throw new \LogicException('Unable to add a node visitor as extensions have already been initialized.'); + } + + $this->staging->addNodeVisitor($visitor); + } + + /** + * @return NodeVisitorInterface[] + */ + public function getNodeVisitors(): array + { + if (!$this->initialized) { + $this->initExtensions(); + } + + return $this->visitors; + } + + public function addTokenParser(TokenParserInterface $parser): void + { + if ($this->initialized) { + throw new \LogicException('Unable to add a token parser as extensions have already been initialized.'); + } + + $this->staging->addTokenParser($parser); + } + + /** + * @return TokenParserInterface[] + */ + public function getTokenParsers(): array + { + if (!$this->initialized) { + $this->initExtensions(); + } + + return $this->parsers; + } + + public function getTokenParser(string $name): ?TokenParserInterface + { + if (!$this->initialized) { + $this->initExtensions(); + } + + if (isset($this->parsers[$name])) { + return $this->parsers[$name]; + } + + foreach ($this->parserCallbacks as $callback) { + if (false !== $parser = $callback($name)) { + return $parser; + } + } + + return null; + } + + /** + * @param callable(string): (TokenParserInterface|false) $callable + */ + public function registerUndefinedTokenParserCallback(callable $callable): void + { + $this->parserCallbacks[] = $callable; + } + + /** + * @return array + */ + public function getGlobals(): array + { + if (null !== $this->globals) { + return $this->globals; + } + + $globals = []; + foreach ($this->extensions as $extension) { + if (!$extension instanceof GlobalsInterface) { + continue; + } + + $globals = array_merge($globals, $extension->getGlobals()); + } + + if ($this->initialized) { + $this->globals = $globals; + } + + return $globals; + } + + public function resetGlobals(): void + { + $this->globals = null; + } + + public function addTest(TwigTest $test): void + { + if ($this->initialized) { + throw new \LogicException(\sprintf('Unable to add test "%s" as extensions have already been initialized.', $test->getName())); + } + + $this->staging->addTest($test); + } + + /** + * @return TwigTest[] + */ + public function getTests(): array + { + if (!$this->initialized) { + $this->initExtensions(); + } + + return $this->tests; + } + + public function getTest(string $name): ?TwigTest + { + if (!$this->initialized) { + $this->initExtensions(); + } + + if (isset($this->tests[$name])) { + return $this->tests[$name]; + } + + foreach ($this->dynamicTests as $pattern => $test) { + if (preg_match($pattern, $name, $matches)) { + array_shift($matches); + + return $test->withDynamicArguments($name, $test->getName(), $matches); + } + } + + foreach ($this->testCallbacks as $callback) { + if (false !== $test = $callback($name)) { + return $test; + } + } + + return null; + } + + /** + * @param callable(string): (TwigTest|false) $callable + */ + public function registerUndefinedTestCallback(callable $callable): void + { + $this->testCallbacks[] = $callable; + } + + public function getExpressionParsers(): ExpressionParsers + { + if (!$this->initialized) { + $this->initExtensions(); + } + + return $this->expressionParsers; + } + + private function initExtensions(): void + { + $this->parsers = []; + $this->filters = []; + $this->functions = []; + $this->tests = []; + $this->dynamicFilters = []; + $this->dynamicFunctions = []; + $this->dynamicTests = []; + $this->visitors = []; + $this->expressionParsers = new ExpressionParsers(); + + foreach ($this->extensions as $extension) { + $this->initExtension($extension); + } + $this->initExtension($this->staging); + // Done at the end only, so that an exception during initialization does not mark the environment as initialized when catching the exception + $this->initialized = true; + } + + private function initExtension(ExtensionInterface $extension): void + { + // filters + foreach ($extension->getFilters() as $filter) { + $this->filters[$name = $filter->getName()] = $filter; + if (str_contains($name, '*')) { + $this->dynamicFilters['#^'.str_replace('\\*', '(.*?)', preg_quote($name, '#')).'$#'] = $filter; + } + } + + // functions + foreach ($extension->getFunctions() as $function) { + $this->functions[$name = $function->getName()] = $function; + if (str_contains($name, '*')) { + $this->dynamicFunctions['#^'.str_replace('\\*', '(.*?)', preg_quote($name, '#')).'$#'] = $function; + } + } + + // tests + foreach ($extension->getTests() as $test) { + $this->tests[$name = $test->getName()] = $test; + if (str_contains($name, '*')) { + $this->dynamicTests['#^'.str_replace('\\*', '(.*?)', preg_quote($name, '#')).'$#'] = $test; + } + } + + // token parsers + foreach ($extension->getTokenParsers() as $parser) { + if (!$parser instanceof TokenParserInterface) { + throw new \LogicException('getTokenParsers() must return an array of \Twig\TokenParser\TokenParserInterface.'); + } + + $this->parsers[$parser->getTag()] = $parser; + } + + // node visitors + foreach ($extension->getNodeVisitors() as $visitor) { + $this->visitors[] = $visitor; + } + + // expression parsers + if (method_exists($extension, 'getExpressionParsers')) { + $this->expressionParsers->add($extension->getExpressionParsers()); + } + + $operators = $extension->getOperators(); + if (!\is_array($operators)) { + throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array with operators, got "%s".', $extension::class, get_debug_type($operators).(\is_resource($operators) ? '' : '#'.$operators))); + } + + if (2 !== \count($operators)) { + throw new \InvalidArgumentException(\sprintf('"%s::getOperators()" must return an array of 2 elements, got %d.', $extension::class, \count($operators))); + } + + $expressionParsers = []; + foreach ($operators[0] as $operator => $op) { + $expressionParsers[] = new UnaryOperatorExpressionParser($op['class'], $operator, $op['precedence'], $op['precedence_change'] ?? null, '', $op['aliases'] ?? []); + } + foreach ($operators[1] as $operator => $op) { + $op['associativity'] = match ($op['associativity']) { + 1 => InfixAssociativity::Left, + 2 => InfixAssociativity::Right, + default => throw new \InvalidArgumentException(\sprintf('Invalid associativity "%s" for operator "%s".', $op['associativity'], $operator)), + }; + + if (isset($op['callable'])) { + $expressionParsers[] = $this->convertInfixExpressionParser($op['class'], $operator, $op['precedence'], $op['associativity'], $op['precedence_change'] ?? null, $op['aliases'] ?? [], $op['callable']); + } else { + $expressionParsers[] = new BinaryOperatorExpressionParser($op['class'], $operator, $op['precedence'], $op['associativity'], $op['precedence_change'] ?? null, '', $op['aliases'] ?? []); + } + } + + if (\count($expressionParsers)) { + trigger_deprecation('twig/twig', '3.21', \sprintf('Extension "%s" uses the old signature for "getOperators()", please implement "getExpressionParsers()" instead.', $extension::class)); + + $this->expressionParsers->add($expressionParsers); + } + } + + private function convertInfixExpressionParser(string $nodeClass, string $operator, int $precedence, InfixAssociativity $associativity, ?PrecedenceChange $precedenceChange, array $aliases, callable $callable): InfixExpressionParserInterface + { + trigger_deprecation('twig/twig', '3.21', \sprintf('Using a non-ExpressionParserInterface object to define the "%s" binary operator is deprecated.', $operator)); + + return new class($nodeClass, $operator, $precedence, $associativity, $precedenceChange, $aliases, $callable) extends BinaryOperatorExpressionParser { + public function __construct( + string $nodeClass, + string $operator, + int $precedence, + InfixAssociativity $associativity = InfixAssociativity::Left, + ?PrecedenceChange $precedenceChange = null, + array $aliases = [], + private $callable = null, + ) { + parent::__construct($nodeClass, $operator, $precedence, $associativity, $precedenceChange, $aliases); + } + + public function parse(Parser $parser, AbstractExpression $expr, Token $token): AbstractExpression + { + return ($this->callable)($parser, $expr); + } + }; + } +} diff --git a/novaconium/vendor/twig/src/FileExtensionEscapingStrategy.php b/novaconium/vendor/twig/src/FileExtensionEscapingStrategy.php new file mode 100644 index 0000000..2785ab7 --- /dev/null +++ b/novaconium/vendor/twig/src/FileExtensionEscapingStrategy.php @@ -0,0 +1,61 @@ + + */ +class FileExtensionEscapingStrategy +{ + /** + * Guesses the best autoescaping strategy based on the file name. + * + * @param string $name The template name + * + * @return string|false The escaping strategy name to use or false to disable + */ + public static function guess(string $name) + { + if (\in_array(substr($name, -1), ['/', '\\'], true)) { + return 'html'; // return html for directories + } + + if (str_ends_with($name, '.twig')) { + $name = substr($name, 0, -5); + } + + $extension = pathinfo($name, \PATHINFO_EXTENSION); + + switch ($extension) { + case 'js': + case 'json': + return 'js'; + + case 'css': + return 'css'; + + case 'txt': + return false; + + default: + return 'html'; + } + } +} diff --git a/novaconium/vendor/twig/src/Lexer.php b/novaconium/vendor/twig/src/Lexer.php new file mode 100644 index 0000000..f208723 --- /dev/null +++ b/novaconium/vendor/twig/src/Lexer.php @@ -0,0 +1,621 @@ + + */ +class Lexer +{ + private $isInitialized = false; + + private $tokens; + private $code; + private $cursor; + private $lineno; + private $end; + private $state; + private $states; + private $brackets; + private $env; + private $source; + private $options; + private $regexes; + private $position; + private $positions; + private $currentVarBlockLine; + private array $openingBrackets = ['{', '(', '[']; + private array $closingBrackets = ['}', ')', ']']; + + public const STATE_DATA = 0; + public const STATE_BLOCK = 1; + public const STATE_VAR = 2; + public const STATE_STRING = 3; + public const STATE_INTERPOLATION = 4; + + public const REGEX_NAME = '/[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/A'; + public const REGEX_STRING = '/"([^#"\\\\]*(?:\\\\.[^#"\\\\]*)*)"|\'([^\'\\\\]*(?:\\\\.[^\'\\\\]*)*)\'/As'; + + public const REGEX_NUMBER = '/(?(DEFINE) + (?[0-9]+(_[0-9]+)*) # Integers (with underscores) 123_456 + (?\.(?&LNUM)) # Fractional part .456 + (?[eE][+-]?(?&LNUM)) # Exponent part E+10 + (?(?&LNUM)(?:(?&FRAC))?) # Decimal number 123_456.456 + )(?:(?&DNUM)(?:(?&EXPONENT))?) # 123_456.456E+10 + /Ax'; + + public const REGEX_DQ_STRING_DELIM = '/"/A'; + public const REGEX_DQ_STRING_PART = '/[^#"\\\\]*(?:(?:\\\\.|#(?!\{))[^#"\\\\]*)*/As'; + public const REGEX_INLINE_COMMENT = '/#[^\n]*/A'; + public const PUNCTUATION = '()[]{}?:.,|'; + + private const REGEX_RAW_INLINE_COMMENT = '/#[^\r\n]*/A'; + + private const SPECIAL_CHARS = [ + 'f' => "\f", + 'n' => "\n", + 'r' => "\r", + 't' => "\t", + 'v' => "\v", + ]; + + public function __construct(Environment $env, array $options = []) + { + $this->env = $env; + + $this->options = array_merge([ + 'tag_comment' => ['{#', '#}'], + 'tag_block' => ['{%', '%}'], + 'tag_variable' => ['{{', '}}'], + 'whitespace_trim' => '-', + 'whitespace_line_trim' => '~', + 'whitespace_line_chars' => ' \t\0\x0B', + 'interpolation' => ['#{', '}'], + ], $options); + } + + private function initialize(): void + { + if ($this->isInitialized) { + return; + } + + // when PHP 7.3 is the min version, we will be able to remove the '#' part in preg_quote as it's part of the default + $this->regexes = [ + // }} + 'lex_var' => '{ + \s* + (?:'. + preg_quote($this->options['whitespace_trim'].$this->options['tag_variable'][1], '#').'\s*'. // -}}\s* + '|'. + preg_quote($this->options['whitespace_line_trim'].$this->options['tag_variable'][1], '#').'['.$this->options['whitespace_line_chars'].']*'. // ~}}[ \t\0\x0B]* + '|'. + preg_quote($this->options['tag_variable'][1], '#'). // }} + ') + }Ax', + + // %} + 'lex_block' => '{ + \s* + (?:'. + preg_quote($this->options['whitespace_trim'].$this->options['tag_block'][1], '#').'\s*\n?'. // -%}\s*\n? + '|'. + preg_quote($this->options['whitespace_line_trim'].$this->options['tag_block'][1], '#').'['.$this->options['whitespace_line_chars'].']*'. // ~%}[ \t\0\x0B]* + '|'. + preg_quote($this->options['tag_block'][1], '#').'(?:\r\n?|\n)?'. // %}(?:\r\n?|\n)? + ') + }Ax', + + // {% endverbatim %} + 'lex_raw_data' => '{'. + preg_quote($this->options['tag_block'][0], '#'). // {% + '('. + $this->options['whitespace_trim']. // - + '|'. + $this->options['whitespace_line_trim']. // ~ + ')?\s*endverbatim\s*'. + '(?:'. + preg_quote($this->options['whitespace_trim'].$this->options['tag_block'][1], '#').'\s*'. // -%} + '|'. + preg_quote($this->options['whitespace_line_trim'].$this->options['tag_block'][1], '#').'['.$this->options['whitespace_line_chars'].']*'. // ~%}[ \t\0\x0B]* + '|'. + preg_quote($this->options['tag_block'][1], '#'). // %} + ') + }sx', + + 'operator' => $this->getOperatorRegex(), + + // #} + 'lex_comment' => '{ + (?:'. + preg_quote($this->options['whitespace_trim'].$this->options['tag_comment'][1], '#').'\s*\n?'. // -#}\s*\n? + '|'. + preg_quote($this->options['whitespace_line_trim'].$this->options['tag_comment'][1], '#').'['.$this->options['whitespace_line_chars'].']*'. // ~#}[ \t\0\x0B]* + '|'. + preg_quote($this->options['tag_comment'][1], '#').'(?:\r\n?|\n)?'. // #}(?:\r\n?|\n)? + ') + }sx', + + // verbatim %} + 'lex_block_raw' => '{ + \s*verbatim\s* + (?:'. + preg_quote($this->options['whitespace_trim'].$this->options['tag_block'][1], '#').'\s*'. // -%}\s* + '|'. + preg_quote($this->options['whitespace_line_trim'].$this->options['tag_block'][1], '#').'['.$this->options['whitespace_line_chars'].']*'. // ~%}[ \t\0\x0B]* + '|'. + preg_quote($this->options['tag_block'][1], '#'). // %} + ') + }Asx', + + 'lex_block_line' => '{\s*line\s+(\d+)\s*'.preg_quote($this->options['tag_block'][1], '#').'}As', + + // {{ or {% or {# + 'lex_tokens_start' => '{ + ('. + preg_quote($this->options['tag_variable'][0], '#'). // {{ + '|'. + preg_quote($this->options['tag_block'][0], '#'). // {% + '|'. + preg_quote($this->options['tag_comment'][0], '#'). // {# + ')('. + preg_quote($this->options['whitespace_trim'], '#'). // - + '|'. + preg_quote($this->options['whitespace_line_trim'], '#'). // ~ + ')? + }sx', + 'interpolation_start' => '{'.preg_quote($this->options['interpolation'][0], '#').'\s*}A', + 'interpolation_end' => '{\s*'.preg_quote($this->options['interpolation'][1], '#').'}A', + ]; + + $this->isInitialized = true; + } + + public function tokenize(Source $source): TokenStream + { + $this->initialize(); + + $this->source = $source; + $this->code = $source->getCode(); + $this->cursor = 0; + $this->lineno = 1; + $this->end = \strlen($this->code); + $this->tokens = []; + $this->state = self::STATE_DATA; + $this->states = []; + $this->brackets = []; + $this->position = -1; + + // find all token starts in one go + preg_match_all($this->regexes['lex_tokens_start'], $this->code, $matches, \PREG_OFFSET_CAPTURE); + $this->positions = $matches; + + while ($this->cursor < $this->end) { + // dispatch to the lexing functions depending + // on the current state + switch ($this->state) { + case self::STATE_DATA: + $this->lexData(); + break; + + case self::STATE_BLOCK: + $this->lexBlock(); + break; + + case self::STATE_VAR: + $this->lexVar(); + break; + + case self::STATE_STRING: + $this->lexString(); + break; + + case self::STATE_INTERPOLATION: + $this->lexInterpolation(); + break; + } + } + + $this->pushToken(Token::EOF_TYPE); + + if ($this->brackets) { + [$expect, $lineno] = array_pop($this->brackets); + throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $lineno, $this->source); + } + + return new TokenStream($this->tokens, $this->source); + } + + private function lexData(): void + { + // if no matches are left we return the rest of the template as simple text token + if ($this->position == \count($this->positions[0]) - 1) { + $text = substr($this->code, $this->cursor); + $this->pushToken(Token::TEXT_TYPE, $this->normalizeNewlines($text)); + $this->moveCursor($text); + + return; + } + + // Find the first token after the current cursor + $position = $this->positions[0][++$this->position]; + while ($position[1] < $this->cursor) { + if ($this->position == \count($this->positions[0]) - 1) { + return; + } + $position = $this->positions[0][++$this->position]; + } + + // push the template text first + $text = $textContent = substr($this->code, $this->cursor, $position[1] - $this->cursor); + + // trim? + if (isset($this->positions[2][$this->position][0])) { + if ($this->options['whitespace_trim'] === $this->positions[2][$this->position][0]) { + // whitespace_trim detected ({%-, {{- or {#-) + $text = rtrim($text); + } elseif ($this->options['whitespace_line_trim'] === $this->positions[2][$this->position][0]) { + // whitespace_line_trim detected ({%~, {{~ or {#~) + // don't trim \r and \n + $text = rtrim($text, " \t\0\x0B"); + } + } + $this->pushToken(Token::TEXT_TYPE, $this->normalizeNewlines($text)); + $this->moveCursor($textContent); + + switch ($this->positions[1][$this->position][0]) { + case $this->options['tag_comment'][0]: + $this->moveCursor($position[0]); + $this->lexComment(); + break; + + case $this->options['tag_block'][0]: + $lineno = $this->lineno; + $cursor = $this->cursor; + $this->moveCursor($position[0]); + + // raw data? + if (preg_match($this->regexes['lex_block_raw'], $this->code, $match, 0, $this->cursor)) { + $this->moveCursor($match[0]); + $this->lexRawData(); + // {% line \d+ %} + } elseif (preg_match($this->regexes['lex_block_line'], $this->code, $match, 0, $this->cursor)) { + $this->moveCursor($match[0]); + $this->lineno = (int) $match[1]; + } else { + $this->pushToken(Token::BLOCK_START_TYPE, '', $cursor, $lineno); + $this->pushState(self::STATE_BLOCK); + $this->currentVarBlockLine = $this->lineno; + } + break; + + case $this->options['tag_variable'][0]: + $lineno = $this->lineno; + $cursor = $this->cursor; + $this->moveCursor($position[0]); + $this->pushToken(Token::VAR_START_TYPE, '', $cursor, $lineno); + $this->pushState(self::STATE_VAR); + $this->currentVarBlockLine = $this->lineno; + break; + } + } + + private function lexBlock(): void + { + if (!$this->brackets && preg_match($this->regexes['lex_block'], $this->code, $match, 0, $this->cursor)) { + $this->pushClosingToken(Token::BLOCK_END_TYPE, $match[0]); + $this->popState(); + } else { + $this->lexExpression(); + } + } + + private function lexVar(): void + { + if (!$this->brackets && preg_match($this->regexes['lex_var'], $this->code, $match, 0, $this->cursor)) { + $this->pushClosingToken(Token::VAR_END_TYPE, $match[0]); + $this->popState(); + } else { + $this->lexExpression(); + } + } + + private function lexExpression(): void + { + // whitespace + if (preg_match('/\s+/A', $this->code, $match, 0, $this->cursor)) { + $this->moveCursor($match[0]); + + if ($this->cursor >= $this->end) { + throw new SyntaxError(\sprintf('Unclosed "%s".', self::STATE_BLOCK === $this->state ? 'block' : 'variable'), $this->currentVarBlockLine, $this->source); + } + } + + // operators + if (preg_match($this->regexes['operator'], $this->code, $match, 0, $this->cursor)) { + $operator = preg_replace('/\s+/', ' ', $match[0]); + if (\in_array($operator, $this->openingBrackets, true)) { + $this->checkBrackets($operator); + } + $this->pushToken(Token::OPERATOR_TYPE, $operator); + $this->moveCursor($match[0]); + } + // names + elseif (preg_match(self::REGEX_NAME, $this->code, $match, 0, $this->cursor)) { + $this->pushToken(Token::NAME_TYPE, $match[0]); + $this->moveCursor($match[0]); + } + // numbers + elseif (preg_match(self::REGEX_NUMBER, $this->code, $match, 0, $this->cursor)) { + $this->pushToken(Token::NUMBER_TYPE, 0 + str_replace('_', '', $match[0])); + $this->moveCursor($match[0]); + } + // punctuation + elseif (str_contains(self::PUNCTUATION, $this->code[$this->cursor])) { + $this->checkBrackets($this->code[$this->cursor]); + $this->pushToken(Token::PUNCTUATION_TYPE, $this->code[$this->cursor]); + $this->moveCursor($this->code[$this->cursor]); + } + // strings + elseif (preg_match(self::REGEX_STRING, $this->code, $match, 0, $this->cursor)) { + $this->pushToken(Token::STRING_TYPE, $this->stripcslashes($this->normalizeNewlines(substr($match[0], 1, -1)), substr($match[0], 0, 1))); + $this->moveCursor($match[0]); + } + // opening double quoted string + elseif (preg_match(self::REGEX_DQ_STRING_DELIM, $this->code, $match, 0, $this->cursor)) { + $this->brackets[] = ['"', $this->lineno]; + $this->pushState(self::STATE_STRING); + $this->moveCursor($match[0]); + } + // inline comment + elseif (preg_match(self::REGEX_RAW_INLINE_COMMENT, $this->code, $match, 0, $this->cursor)) { + $this->moveCursor($match[0]); + } + // unlexable + else { + throw new SyntaxError(\sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source, columnno: $this->source->getColumn($this->cursor)); + } + } + + private function stripcslashes(string $str, string $quoteType): string + { + $result = ''; + $length = \strlen($str); + + $i = 0; + while ($i < $length) { + if (false === $pos = strpos($str, '\\', $i)) { + $result .= substr($str, $i); + break; + } + + $result .= substr($str, $i, $pos - $i); + $i = $pos + 1; + + if ($i >= $length) { + $result .= '\\'; + break; + } + + $nextChar = $str[$i]; + + if (isset(self::SPECIAL_CHARS[$nextChar])) { + $result .= self::SPECIAL_CHARS[$nextChar]; + } elseif ('\\' === $nextChar) { + $result .= $nextChar; + } elseif ("'" === $nextChar || '"' === $nextChar) { + if ($nextChar !== $quoteType) { + trigger_deprecation('twig/twig', '3.12', 'Character "%s" should not be escaped; the "\" character is ignored in Twig 3 but will not be in Twig 4. Please remove the extra "\" character at position %d in "%s" at line %d.', $nextChar, $i + 1, $this->source->getName(), $this->lineno); + } + $result .= $nextChar; + } elseif ('#' === $nextChar && $i + 1 < $length && '{' === $str[$i + 1]) { + $result .= '#{'; + ++$i; + } elseif ('x' === $nextChar && $i + 1 < $length && ctype_xdigit($str[$i + 1])) { + $hex = $str[++$i]; + if ($i + 1 < $length && ctype_xdigit($str[$i + 1])) { + $hex .= $str[++$i]; + } + $result .= \chr(hexdec($hex)); + } elseif (ctype_digit($nextChar) && $nextChar < '8') { + $octal = $nextChar; + while ($i + 1 < $length && ctype_digit($str[$i + 1]) && $str[$i + 1] < '8' && \strlen($octal) < 3) { + $octal .= $str[++$i]; + } + $result .= \chr(octdec($octal) % 256); + } else { + trigger_deprecation('twig/twig', '3.12', 'Character "%s" should not be escaped; the "\" character is ignored in Twig 3 but will not be in Twig 4. Please remove the extra "\" character at position %d in "%s" at line %d.', $nextChar, $i + 1, $this->source->getName(), $this->lineno); + $result .= $nextChar; + } + + ++$i; + } + + return $result; + } + + private function lexRawData(): void + { + if (!preg_match($this->regexes['lex_raw_data'], $this->code, $match, \PREG_OFFSET_CAPTURE, $this->cursor)) { + throw new SyntaxError('Unexpected end of file: Unclosed "verbatim" block.', $this->lineno, $this->source); + } + + $offset = $this->cursor; + $text = substr($this->code, $this->cursor, $match[0][1] - $this->cursor); + $this->moveCursor($text.$match[0][0]); + + // trim? + if (isset($match[1][0])) { + if ($this->options['whitespace_trim'] === $match[1][0]) { + // whitespace_trim detected ({%-, {{- or {#-) + $text = rtrim($text); + } else { + // whitespace_line_trim detected ({%~, {{~ or {#~) + // don't trim \r and \n + $text = rtrim($text, " \t\0\x0B"); + } + } + + $this->pushToken(Token::TEXT_TYPE, $this->normalizeNewlines($text), $offset); + } + + private function lexComment(): void + { + if (!preg_match($this->regexes['lex_comment'], $this->code, $match, \PREG_OFFSET_CAPTURE, $this->cursor)) { + throw new SyntaxError('Unclosed comment.', $this->lineno, $this->source); + } + + $this->moveCursor(substr($this->code, $this->cursor, $match[0][1] - $this->cursor).$match[0][0]); + } + + private function lexString(): void + { + if (preg_match($this->regexes['interpolation_start'], $this->code, $match, 0, $this->cursor)) { + $this->brackets[] = [$this->options['interpolation'][0], $this->lineno]; + $this->pushToken(Token::INTERPOLATION_START_TYPE); + $this->moveCursor($match[0]); + $this->pushState(self::STATE_INTERPOLATION); + } elseif (preg_match(self::REGEX_DQ_STRING_PART, $this->code, $match, 0, $this->cursor) && '' !== $match[0]) { + $this->pushToken(Token::STRING_TYPE, $this->stripcslashes($this->normalizeNewlines($match[0]), '"')); + $this->moveCursor($match[0]); + } elseif (preg_match(self::REGEX_DQ_STRING_DELIM, $this->code, $match, 0, $this->cursor)) { + [$expect, $lineno] = array_pop($this->brackets); + if ('"' != $this->code[$this->cursor]) { + throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $lineno, $this->source); + } + + $this->popState(); + $this->moveCursor($match[0]); + } else { + // unlexable + throw new SyntaxError(\sprintf('Unexpected character "%s".', $this->code[$this->cursor]), $this->lineno, $this->source, columnno: $this->source->getColumn($this->cursor)); + } + } + + private function lexInterpolation(): void + { + $bracket = end($this->brackets); + if ($this->options['interpolation'][0] === $bracket[0] && preg_match($this->regexes['interpolation_end'], $this->code, $match, 0, $this->cursor)) { + array_pop($this->brackets); + $this->pushClosingToken(Token::INTERPOLATION_END_TYPE, $match[0]); + $this->popState(); + } else { + $this->lexExpression(); + } + } + + private function pushToken($type, $value = '', ?int $offset = null, ?int $lineno = null): void + { + // do not push empty text tokens + if (Token::TEXT_TYPE === $type && '' === $value) { + return; + } + + // by default the token starts at the current cursor; callers that + // emit a token after consuming it must pass an explicit offset + $this->tokens[] = new Token($type, $value, $lineno ?? $this->lineno, $offset ?? $this->cursor); + } + + private function moveCursor($text): void + { + $this->cursor += \strlen($text); + // count "\r\n" and "\r" as a single newline without allocating a + // normalized copy when the chunk has no carriage return (common case) + $this->lineno += str_contains($text, "\r") ? substr_count($this->normalizeNewlines($text), "\n") : substr_count($text, "\n"); + } + + private function normalizeNewlines(string $text): string + { + return str_replace(["\r\n", "\r"], "\n", $text); + } + + private function pushClosingToken(int $type, string $match): void + { + $leadingWhitespaceLength = \strlen($match) - \strlen(ltrim($match)); + if ($leadingWhitespaceLength) { + $this->moveCursor(substr($match, 0, $leadingWhitespaceLength)); + } + + $this->pushToken($type); + $this->moveCursor(substr($match, $leadingWhitespaceLength)); + } + + private function getOperatorRegex(): string + { + $expressionParsers = []; + foreach ($this->env->getExpressionParsers() as $expressionParser) { + $expressionParsers = array_merge($expressionParsers, ExpressionParsers::getOperatorTokensFor($expressionParser)); + } + + $expressionParsers = array_combine($expressionParsers, array_map('strlen', $expressionParsers)); + arsort($expressionParsers); + + $regex = []; + foreach ($expressionParsers as $expressionParser => $length) { + // an operator that ends with a character must be followed by + // a whitespace, a parenthesis, an opening map [ or sequence { + $r = preg_quote($expressionParser, '/'); + if (ctype_alpha($expressionParser[$length - 1])) { + $r .= '(?=[\s()\[{])'; + } + + // an operator that begins with a character must not have a dot or pipe before + if (ctype_alpha($expressionParser[0])) { + $r = '(?states[] = $this->state; + $this->state = $state; + } + + private function popState(): void + { + if (0 === \count($this->states)) { + throw new \LogicException('Cannot pop state without a previous state.'); + } + + $this->state = array_pop($this->states); + } + + private function checkBrackets(string $code): void + { + // opening bracket + if (\in_array($code, $this->openingBrackets, true)) { + $this->brackets[] = [$code, $this->lineno]; + } elseif (\in_array($code, $this->closingBrackets, true)) { + // closing bracket + if (!$this->brackets) { + throw new SyntaxError(\sprintf('Unexpected "%s".', $code), $this->lineno, $this->source, columnno: $this->source->getColumn($this->cursor)); + } + + [$expect, $lineno] = array_pop($this->brackets); + if ($code !== str_replace($this->openingBrackets, $this->closingBrackets, $expect)) { + throw new SyntaxError(\sprintf('Unclosed "%s".', $expect), $lineno, $this->source); + } + } + } +} diff --git a/novaconium/vendor/twig/src/Loader/ArrayLoader.php b/novaconium/vendor/twig/src/Loader/ArrayLoader.php new file mode 100644 index 0000000..2bb54b7 --- /dev/null +++ b/novaconium/vendor/twig/src/Loader/ArrayLoader.php @@ -0,0 +1,75 @@ + + */ +final class ArrayLoader implements LoaderInterface +{ + /** + * @param array $templates An array of templates (keys are the names, and values are the source code) + */ + public function __construct( + private array $templates = [], + ) { + } + + public function setTemplate(string $name, string $template): void + { + $this->templates[$name] = $template; + } + + public function getSourceContext(string $name): Source + { + if (!isset($this->templates[$name])) { + throw new LoaderError(\sprintf('Template "%s" is not defined.', $name)); + } + + return new Source($this->templates[$name], $name); + } + + public function exists(string $name): bool + { + return isset($this->templates[$name]); + } + + public function getCacheKey(string $name): string + { + if (!isset($this->templates[$name])) { + throw new LoaderError(\sprintf('Template "%s" is not defined.', $name)); + } + + return $name.':'.$this->templates[$name]; + } + + public function isFresh(string $name, int $time): bool + { + if (!isset($this->templates[$name])) { + throw new LoaderError(\sprintf('Template "%s" is not defined.', $name)); + } + + return true; + } +} diff --git a/novaconium/vendor/twig/src/Loader/ChainLoader.php b/novaconium/vendor/twig/src/Loader/ChainLoader.php new file mode 100644 index 0000000..0859dcd --- /dev/null +++ b/novaconium/vendor/twig/src/Loader/ChainLoader.php @@ -0,0 +1,132 @@ + + */ +final class ChainLoader implements LoaderInterface +{ + /** + * @var array + */ + private $hasSourceCache = []; + + /** + * @param iterable $loaders + */ + public function __construct( + private iterable $loaders = [], + ) { + } + + public function addLoader(LoaderInterface $loader): void + { + $current = $this->loaders; + + $this->loaders = (static function () use ($current, $loader): \Generator { + yield from $current; + yield $loader; + })(); + + $this->hasSourceCache = []; + } + + /** + * @return LoaderInterface[] + */ + public function getLoaders(): array + { + if (!\is_array($this->loaders)) { + $this->loaders = iterator_to_array($this->loaders, false); + } + + return $this->loaders; + } + + public function getSourceContext(string $name): Source + { + $exceptions = []; + + foreach ($this->getLoaders() as $loader) { + if (!$loader->exists($name)) { + continue; + } + + try { + return $loader->getSourceContext($name); + } catch (LoaderError $e) { + $exceptions[] = $e->getMessage(); + } + } + + throw new LoaderError(\sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : '')); + } + + public function exists(string $name): bool + { + if (isset($this->hasSourceCache[$name])) { + return $this->hasSourceCache[$name]; + } + + foreach ($this->getLoaders() as $loader) { + if ($loader->exists($name)) { + return $this->hasSourceCache[$name] = true; + } + } + + return $this->hasSourceCache[$name] = false; + } + + public function getCacheKey(string $name): string + { + $exceptions = []; + + foreach ($this->getLoaders() as $loader) { + if (!$loader->exists($name)) { + continue; + } + + try { + return $loader->getCacheKey($name); + } catch (LoaderError $e) { + $exceptions[] = $loader::class.': '.$e->getMessage(); + } + } + + throw new LoaderError(\sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : '')); + } + + public function isFresh(string $name, int $time): bool + { + $exceptions = []; + + foreach ($this->getLoaders() as $loader) { + if (!$loader->exists($name)) { + continue; + } + + try { + return $loader->isFresh($name, $time); + } catch (LoaderError $e) { + $exceptions[] = $loader::class.': '.$e->getMessage(); + } + } + + throw new LoaderError(\sprintf('Template "%s" is not defined%s.', $name, $exceptions ? ' ('.implode(', ', $exceptions).')' : '')); + } +} diff --git a/novaconium/vendor/twig/src/Loader/FilesystemLoader.php b/novaconium/vendor/twig/src/Loader/FilesystemLoader.php new file mode 100644 index 0000000..49f2b89 --- /dev/null +++ b/novaconium/vendor/twig/src/Loader/FilesystemLoader.php @@ -0,0 +1,290 @@ + + */ +class FilesystemLoader implements LoaderInterface +{ + /** Identifier of the main namespace. */ + public const MAIN_NAMESPACE = '__main__'; + + /** + * @var array> + */ + protected $paths = []; + protected $cache = []; + protected $errorCache = []; + + private $rootPath; + + /** + * @param string|string[] $paths A path or an array of paths where to look for templates + * @param string|null $rootPath The root path common to all relative paths (null for getcwd()) + */ + public function __construct($paths = [], ?string $rootPath = null) + { + $this->rootPath = ($rootPath ?? getcwd()).\DIRECTORY_SEPARATOR; + if (null !== $rootPath && false !== ($realPath = realpath($rootPath))) { + $this->rootPath = $realPath.\DIRECTORY_SEPARATOR; + } + + if ($paths) { + $this->setPaths($paths); + } + } + + /** + * Returns the paths to the templates. + * + * @return list + */ + public function getPaths(string $namespace = self::MAIN_NAMESPACE): array + { + return $this->paths[$namespace] ?? []; + } + + /** + * Returns the path namespaces. + * + * The main namespace is always defined. + * + * @return list + */ + public function getNamespaces(): array + { + return array_keys($this->paths); + } + + /** + * @param string|string[] $paths A path or an array of paths where to look for templates + */ + public function setPaths($paths, string $namespace = self::MAIN_NAMESPACE): void + { + if (!\is_array($paths)) { + $paths = [$paths]; + } + + $this->paths[$namespace] = []; + foreach ($paths as $path) { + $this->addPath($path, $namespace); + } + } + + /** + * @throws LoaderError + */ + public function addPath(string $path, string $namespace = self::MAIN_NAMESPACE): void + { + // invalidate the cache + $this->cache = $this->errorCache = []; + + $checkPath = $this->isAbsolutePath($path) ? $path : $this->rootPath.$path; + if (!is_dir($checkPath)) { + throw new LoaderError(\sprintf('The "%s" directory does not exist ("%s").', $path, $checkPath)); + } + + $this->paths[$namespace][] = rtrim($path, '/\\'); + } + + /** + * @throws LoaderError + */ + public function prependPath(string $path, string $namespace = self::MAIN_NAMESPACE): void + { + // invalidate the cache + $this->cache = $this->errorCache = []; + + $checkPath = $this->isAbsolutePath($path) ? $path : $this->rootPath.$path; + if (!is_dir($checkPath)) { + throw new LoaderError(\sprintf('The "%s" directory does not exist ("%s").', $path, $checkPath)); + } + + $path = rtrim($path, '/\\'); + + if (!isset($this->paths[$namespace])) { + $this->paths[$namespace][] = $path; + } else { + array_unshift($this->paths[$namespace], $path); + } + } + + public function getSourceContext(string $name): Source + { + if (null === $path = $this->findTemplate($name)) { + return new Source('', $name, ''); + } + + return new Source(file_get_contents($path), $name, $path); + } + + public function getCacheKey(string $name): string + { + if (null === $path = $this->findTemplate($name)) { + return ''; + } + $len = \strlen($this->rootPath); + if (0 === strncmp($this->rootPath, $path, $len)) { + return substr($path, $len); + } + + return $path; + } + + /** + * @return bool + */ + public function exists(string $name) + { + $name = $this->normalizeName($name); + + if (isset($this->cache[$name])) { + return true; + } + + return null !== $this->findTemplate($name, false); + } + + public function isFresh(string $name, int $time): bool + { + // false support to be removed in 3.0 + if (null === $path = $this->findTemplate($name)) { + return false; + } + + return filemtime($path) < $time; + } + + /** + * @return string|null + */ + protected function findTemplate(string $name, bool $throw = true) + { + $name = $this->normalizeName($name); + + if (isset($this->cache[$name])) { + return $this->cache[$name]; + } + + if (isset($this->errorCache[$name])) { + if (!$throw) { + return null; + } + + throw new LoaderError($this->errorCache[$name]); + } + + try { + [$namespace, $shortname] = $this->parseName($name); + + $this->validateName($shortname); + } catch (LoaderError $e) { + if (!$throw) { + return null; + } + + throw $e; + } + + if (!isset($this->paths[$namespace])) { + $this->errorCache[$name] = \sprintf('There are no registered paths for namespace "%s".', $namespace); + + if (!$throw) { + return null; + } + + throw new LoaderError($this->errorCache[$name]); + } + + foreach ($this->paths[$namespace] as $path) { + if (!$this->isAbsolutePath($path)) { + $path = $this->rootPath.$path; + } + + if (is_file($path.'/'.$shortname)) { + if (false !== $realpath = realpath($path.'/'.$shortname)) { + return $this->cache[$name] = $realpath; + } + + return $this->cache[$name] = $path.'/'.$shortname; + } + } + + $this->errorCache[$name] = \sprintf('Unable to find template "%s" (looked into: %s).', $name, implode(', ', $this->paths[$namespace])); + + if (!$throw) { + return null; + } + + throw new LoaderError($this->errorCache[$name]); + } + + private function normalizeName(string $name): string + { + return preg_replace('#/{2,}#', '/', str_replace('\\', '/', $name)); + } + + private function parseName(string $name, string $default = self::MAIN_NAMESPACE): array + { + if (isset($name[0]) && '@' == $name[0]) { + if (false === $pos = strpos($name, '/')) { + throw new LoaderError(\sprintf('Malformed namespaced template name "%s" (expecting "@namespace/template_name").', $name)); + } + + $namespace = substr($name, 1, $pos - 1); + $shortname = substr($name, $pos + 1); + + return [$namespace, $shortname]; + } + + return [$default, $name]; + } + + private function validateName(string $name): void + { + if (str_contains($name, "\0")) { + throw new LoaderError('A template name cannot contain NUL bytes.'); + } + + $name = ltrim($name, '/'); + $parts = explode('/', $name); + $level = 0; + foreach ($parts as $part) { + if ('..' === $part) { + --$level; + } elseif ('.' !== $part) { + ++$level; + } + + if ($level < 0) { + throw new LoaderError(\sprintf('Looks like you try to load a template outside configured directories (%s).', $name)); + } + } + } + + private function isAbsolutePath(string $file): bool + { + return strspn($file, '/\\', 0, 1) + || (\strlen($file) > 3 && ctype_alpha($file[0]) + && ':' === $file[1] + && strspn($file, '/\\', 2, 1) + ) + || null !== parse_url($file, \PHP_URL_SCHEME) + ; + } +} diff --git a/novaconium/vendor/twig/src/Loader/LoaderInterface.php b/novaconium/vendor/twig/src/Loader/LoaderInterface.php new file mode 100644 index 0000000..fec7e85 --- /dev/null +++ b/novaconium/vendor/twig/src/Loader/LoaderInterface.php @@ -0,0 +1,49 @@ + + */ +interface LoaderInterface +{ + /** + * Returns the source context for a given template logical name. + * + * @throws LoaderError When $name is not found + */ + public function getSourceContext(string $name): Source; + + /** + * Gets the cache key to use for the cache for a given template name. + * + * @throws LoaderError When $name is not found + */ + public function getCacheKey(string $name): string; + + /** + * @param int $time Timestamp of the last modification time of the cached template + * + * @throws LoaderError When $name is not found + */ + public function isFresh(string $name, int $time): bool; + + /** + * @return bool + */ + public function exists(string $name); +} diff --git a/novaconium/vendor/twig/src/Markup.php b/novaconium/vendor/twig/src/Markup.php new file mode 100644 index 0000000..820f091 --- /dev/null +++ b/novaconium/vendor/twig/src/Markup.php @@ -0,0 +1,67 @@ + + */ +class Markup implements \Countable, \JsonSerializable, \Stringable +{ + private $content; + private ?string $charset; + + public function __construct($content, $charset) + { + $this->content = (string) $content; + $this->charset = $charset; + } + + public function __toString(): string + { + return $this->content; + } + + public function getCharset(): string + { + return $this->charset; + } + + /** + * @return int + */ + #[\ReturnTypeWillChange] + public function count() + { + return mb_strlen($this->content, $this->charset); + } + + /** + * @return mixed + */ + #[\ReturnTypeWillChange] + public function jsonSerialize() + { + return $this->content; + } +} diff --git a/novaconium/vendor/twig/src/Node/AutoEscapeNode.php b/novaconium/vendor/twig/src/Node/AutoEscapeNode.php new file mode 100644 index 0000000..ee80639 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/AutoEscapeNode.php @@ -0,0 +1,40 @@ + + */ +#[YieldReady] +class AutoEscapeNode extends Node +{ + public function __construct($value, Node $body, int $lineno) + { + parent::__construct(['body' => $body], ['value' => $value], $lineno); + } + + public function compile(Compiler $compiler): void + { + $compiler->subcompile($this->getNode('body')); + } +} diff --git a/novaconium/vendor/twig/src/Node/BlockNode.php b/novaconium/vendor/twig/src/Node/BlockNode.php new file mode 100644 index 0000000..b4f939c --- /dev/null +++ b/novaconium/vendor/twig/src/Node/BlockNode.php @@ -0,0 +1,50 @@ + + */ +#[YieldReady] +class BlockNode extends Node +{ + public function __construct(string $name, Node $body, int $lineno) + { + parent::__construct(['body' => $body], ['name' => $name], $lineno); + } + + public function compile(Compiler $compiler): void + { + $compiler + ->addDebugInfo($this) + ->write("/**\n") + ->write(" * @return iterable\n") + ->write(" */\n") + ->write(\sprintf("public function block_%s(array \$context, array \$blocks = []): iterable\n", $this->getAttribute('name')), "{\n") + ->indent() + ->write("\$macros = \$this->macros;\n") + ; + + $compiler + ->subcompile($this->getNode('body')) + ->write("yield from [];\n") + ->outdent() + ->write("}\n\n") + ; + } +} diff --git a/novaconium/vendor/twig/src/Node/BlockReferenceNode.php b/novaconium/vendor/twig/src/Node/BlockReferenceNode.php new file mode 100644 index 0000000..7c313a0 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/BlockReferenceNode.php @@ -0,0 +1,38 @@ + + */ +#[YieldReady] +class BlockReferenceNode extends Node implements NodeOutputInterface +{ + public function __construct(string $name, int $lineno) + { + parent::__construct([], ['name' => $name], $lineno); + } + + public function compile(Compiler $compiler): void + { + $compiler + ->addDebugInfo($this) + ->write(\sprintf("yield from \$this->unwrap()->yieldBlock('%s', \$context, \$blocks);\n", $this->getAttribute('name'))) + ; + } +} diff --git a/novaconium/vendor/twig/src/Node/BodyNode.php b/novaconium/vendor/twig/src/Node/BodyNode.php new file mode 100644 index 0000000..08115b3 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/BodyNode.php @@ -0,0 +1,24 @@ + + */ +#[YieldReady] +class BodyNode extends Node +{ +} diff --git a/novaconium/vendor/twig/src/Node/CaptureNode.php b/novaconium/vendor/twig/src/Node/CaptureNode.php new file mode 100644 index 0000000..3b7f0b6 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/CaptureNode.php @@ -0,0 +1,57 @@ + + */ +#[YieldReady] +class CaptureNode extends Node +{ + public function __construct(Node $body, int $lineno) + { + parent::__construct(['body' => $body], ['raw' => false], $lineno); + } + + public function compile(Compiler $compiler): void + { + $useYield = $compiler->getEnvironment()->useYield(); + + if (!$this->getAttribute('raw')) { + $compiler->raw("('' === \$tmp = "); + } + $compiler + ->raw($useYield ? "implode('', iterator_to_array(" : '\\Twig\\Extension\\CoreExtension::captureOutput(') + ->raw("(function () use (&\$context, \$macros, \$blocks) {\n") + ->indent() + ->subcompile($this->getNode('body')) + ->write("yield from [];\n") + ->outdent() + ->write('})()') + ; + if ($useYield) { + $compiler->raw(', false))'); + } else { + $compiler->raw(')'); + } + if (!$this->getAttribute('raw')) { + $compiler->raw(") ? '' : new Markup(\$tmp, \$this->env->getCharset());"); + } else { + $compiler->raw(';'); + } + } +} diff --git a/novaconium/vendor/twig/src/Node/CheckSecurityCallNode.php b/novaconium/vendor/twig/src/Node/CheckSecurityCallNode.php new file mode 100644 index 0000000..0667bd0 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/CheckSecurityCallNode.php @@ -0,0 +1,32 @@ + + */ +#[YieldReady] +class CheckSecurityCallNode extends Node +{ + /** + * @return void + */ + public function compile(Compiler $compiler) + { + $compiler + ->write("\$this->sandbox = \$this->extensions[SandboxExtension::class];\n") + ; + } +} diff --git a/novaconium/vendor/twig/src/Node/CheckSecurityNode.php b/novaconium/vendor/twig/src/Node/CheckSecurityNode.php new file mode 100644 index 0000000..92316ae --- /dev/null +++ b/novaconium/vendor/twig/src/Node/CheckSecurityNode.php @@ -0,0 +1,109 @@ + + */ +#[YieldReady] +class CheckSecurityNode extends Node +{ + private $usedFilters; + private $usedTags; + private $usedFunctions; + private $usedTests; + + /** + * @param array $usedFilters + * @param array $usedTags + * @param array $usedFunctions + * @param array $usedTests + */ + public function __construct(array $usedFilters, array $usedTags, array $usedFunctions, array $usedTests = []) + { + if (\func_num_args() < 4) { + trigger_deprecation('twig/twig', '3.28', 'Not passing the "$usedTests" argument to "%s::__construct()" is deprecated; it will be required in 4.0.', static::class); + } + + $this->usedFilters = $usedFilters; + $this->usedTags = $usedTags; + $this->usedFunctions = $usedFunctions; + $this->usedTests = $usedTests; + + parent::__construct(); + } + + public function compile(Compiler $compiler): void + { + $compiler + ->write("\n") + ->write("public function ensureSecurityChecked(): void\n") + ->write("{\n") + ->indent() + ->write("if (\$this->sandbox->isSandboxed(\$this->source)) {\n") + ->indent() + ->write("\$this->checkSecurity();\n") + ->outdent() + ->write("}\n") + ->outdent() + ->write("}\n") + ->write("\n") + ->write("public function checkSecurity()\n") + ->write("{\n") + ->indent() + ->write('static $tags = ')->repr(array_filter($this->usedTags))->raw(";\n") + ->write('static $filters = ')->repr(array_filter($this->usedFilters))->raw(";\n") + ->write('static $functions = ')->repr(array_filter($this->usedFunctions))->raw(";\n") + ->write('static $tests = ')->repr(array_filter($this->usedTests))->raw(";\n\n") + ->write("try {\n") + ->indent() + ->write("\$this->sandbox->checkSecurity(\n") + ->indent() + ->write('')->repr(array_keys($this->usedTags))->raw(",\n") + ->write('')->repr(array_keys($this->usedFilters))->raw(",\n") + ->write('')->repr(array_keys($this->usedFunctions))->raw(",\n") + ->write('')->repr(array_keys($this->usedTests))->raw(",\n") + ->write("\$this->source\n") + ->outdent() + ->write(");\n") + ->outdent() + ->write("} catch (SecurityError \$e) {\n") + ->indent() + ->write("\$e->setSourceContext(\$this->source);\n\n") + ->write("if (\$e instanceof SecurityNotAllowedTagError && isset(\$tags[\$e->getTagName()])) {\n") + ->indent() + ->write("\$e->setTemplateLine(\$tags[\$e->getTagName()]);\n") + ->outdent() + ->write("} elseif (\$e instanceof SecurityNotAllowedFilterError && isset(\$filters[\$e->getFilterName()])) {\n") + ->indent() + ->write("\$e->setTemplateLine(\$filters[\$e->getFilterName()]);\n") + ->outdent() + ->write("} elseif (\$e instanceof SecurityNotAllowedFunctionError && isset(\$functions[\$e->getFunctionName()])) {\n") + ->indent() + ->write("\$e->setTemplateLine(\$functions[\$e->getFunctionName()]);\n") + ->outdent() + ->write("} elseif (\$e instanceof SecurityNotAllowedTestError && isset(\$tests[\$e->getTestName()])) {\n") + ->indent() + ->write("\$e->setTemplateLine(\$tests[\$e->getTestName()]);\n") + ->outdent() + ->write("}\n\n") + ->write("throw \$e;\n") + ->outdent() + ->write("}\n\n") + ->outdent() + ->write("}\n") + ; + } +} diff --git a/novaconium/vendor/twig/src/Node/CheckToStringNode.php b/novaconium/vendor/twig/src/Node/CheckToStringNode.php new file mode 100644 index 0000000..11aec9c --- /dev/null +++ b/novaconium/vendor/twig/src/Node/CheckToStringNode.php @@ -0,0 +1,48 @@ + + */ +#[YieldReady] +class CheckToStringNode extends AbstractExpression +{ + public function __construct(AbstractExpression $expr, bool $spread = false) + { + parent::__construct(['expr' => $expr], ['spread' => $spread], $expr->getTemplateLine()); + } + + public function compile(Compiler $compiler): void + { + $expr = $this->getNode('expr'); + $method = $this->getAttribute('spread') ? 'ensureSpreadAllowed' : 'ensureToStringAllowed'; + $compiler + ->raw('$this->sandbox->'.$method.'(') + ->subcompile($expr) + ->raw(', ') + ->repr($expr->getTemplateLine()) + ->raw(', $this->source)') + ; + } +} diff --git a/novaconium/vendor/twig/src/Node/CoercesChildrenToStringInterface.php b/novaconium/vendor/twig/src/Node/CoercesChildrenToStringInterface.php new file mode 100644 index 0000000..e213d1b --- /dev/null +++ b/novaconium/vendor/twig/src/Node/CoercesChildrenToStringInterface.php @@ -0,0 +1,40 @@ + + */ +interface CoercesChildrenToStringInterface +{ + /** + * Returns the names of the child nodes that will be coerced to + * string when this node is evaluated. + * + * @return string[] + */ + public function getStringCoercedChildNames(): array; +} diff --git a/novaconium/vendor/twig/src/Node/ConfigNode.php b/novaconium/vendor/twig/src/Node/ConfigNode.php new file mode 100644 index 0000000..758e057 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/ConfigNode.php @@ -0,0 +1,30 @@ + + */ +#[YieldReady] +final class ConfigNode extends Node +{ + public function __construct(int $lineno) + { + parent::__construct([], [], $lineno); + } +} diff --git a/novaconium/vendor/twig/src/Node/DeprecatedNode.php b/novaconium/vendor/twig/src/Node/DeprecatedNode.php new file mode 100644 index 0000000..fea8d35 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/DeprecatedNode.php @@ -0,0 +1,87 @@ + + */ +#[YieldReady] +class DeprecatedNode extends Node implements CoercesChildrenToStringInterface +{ + public function __construct(AbstractExpression $expr, int $lineno) + { + parent::__construct(['expr' => $expr], [], $lineno); + } + + public function compile(Compiler $compiler): void + { + $compiler->addDebugInfo($this); + + $expr = $this->getNode('expr'); + + if (!$expr instanceof ConstantExpression) { + $varName = $compiler->getVarName(); + $compiler + ->write(\sprintf('$%s = ', $varName)) + ->subcompile($expr) + ->raw(";\n") + ; + } + + $compiler->write('trigger_deprecation('); + if ($this->hasNode('package')) { + $compiler->subcompile($this->getNode('package')); + } else { + $compiler->raw("''"); + } + $compiler->raw(', '); + if ($this->hasNode('version')) { + $compiler->subcompile($this->getNode('version')); + } else { + $compiler->raw("''"); + } + $compiler->raw(', '); + + if ($expr instanceof ConstantExpression) { + $compiler->subcompile($expr); + } else { + $compiler->write(\sprintf('$%s', $varName)); + } + + $compiler + ->raw('.') + ->string(\sprintf(' in "%s" at line %d.', $this->getTemplateName(), $this->getTemplateLine())) + ->raw(");\n") + ; + } + + public function getStringCoercedChildNames(): array + { + // the message is concatenated with `.`, and `package` / `version` are typed `string` on trigger_deprecation() + $names = ['expr']; + if ($this->hasNode('package')) { + $names[] = 'package'; + } + if ($this->hasNode('version')) { + $names[] = 'version'; + } + + return $names; + } +} diff --git a/novaconium/vendor/twig/src/Node/DoNode.php b/novaconium/vendor/twig/src/Node/DoNode.php new file mode 100644 index 0000000..1593fd0 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/DoNode.php @@ -0,0 +1,40 @@ + + */ +#[YieldReady] +class DoNode extends Node +{ + public function __construct(AbstractExpression $expr, int $lineno) + { + parent::__construct(['expr' => $expr], [], $lineno); + } + + public function compile(Compiler $compiler): void + { + $compiler + ->addDebugInfo($this) + ->write('') + ->subcompile($this->getNode('expr')) + ->raw(";\n") + ; + } +} diff --git a/novaconium/vendor/twig/src/Node/EmbedNode.php b/novaconium/vendor/twig/src/Node/EmbedNode.php new file mode 100644 index 0000000..2de39eb --- /dev/null +++ b/novaconium/vendor/twig/src/Node/EmbedNode.php @@ -0,0 +1,54 @@ + + */ +#[YieldReady] +class EmbedNode extends IncludeNode +{ + // we don't inject the module to avoid node visitors to traverse it twice (as it will be already visited in the main module) + public function __construct(string $name, int $index, ?AbstractExpression $variables, bool $only, bool $ignoreMissing, int $lineno) + { + parent::__construct(new ConstantExpression('not_used', $lineno), $variables, $only, $ignoreMissing, $lineno); + + $this->setAttribute('name', $name); + $this->setAttribute('index', $index); + } + + protected function addGetTemplate(Compiler $compiler, string $template = ''): void + { + $compiler + ->raw('$this->load(') + ->string($this->getAttribute('name')) + ->raw(', ') + ->repr($this->getTemplateLine()) + ->raw(', ') + ->repr($this->getAttribute('index')) + ->raw(')') + ; + if ($this->getAttribute('ignore_missing')) { + $compiler + ->raw(";\n") + ->write(\sprintf("\$%s->getParent(\$context);\n", $template)) + ; + } + } +} diff --git a/novaconium/vendor/twig/src/Node/EmptyNode.php b/novaconium/vendor/twig/src/Node/EmptyNode.php new file mode 100644 index 0000000..fd4717f --- /dev/null +++ b/novaconium/vendor/twig/src/Node/EmptyNode.php @@ -0,0 +1,33 @@ + + */ +#[YieldReady] +final class EmptyNode extends Node +{ + public function __construct(int $lineno = 0) + { + parent::__construct([], [], $lineno); + } + + public function setNode(string $name, Node $node): void + { + throw new \LogicException('EmptyNode cannot have children.'); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/AbstractExpression.php b/novaconium/vendor/twig/src/Node/Expression/AbstractExpression.php new file mode 100644 index 0000000..22d8617 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/AbstractExpression.php @@ -0,0 +1,43 @@ + + */ +abstract class AbstractExpression extends Node +{ + public function isGenerator(): bool + { + return $this->hasAttribute('is_generator') && $this->getAttribute('is_generator'); + } + + /** + * @return static + */ + public function setExplicitParentheses(): self + { + $this->setAttribute('with_parentheses', true); + + return $this; + } + + public function hasExplicitParentheses(): bool + { + return $this->hasAttribute('with_parentheses') && $this->getAttribute('with_parentheses'); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/ArrayExpression.php b/novaconium/vendor/twig/src/Node/Expression/ArrayExpression.php new file mode 100644 index 0000000..189e02f --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/ArrayExpression.php @@ -0,0 +1,165 @@ +index = -1; + foreach ($this->getKeyValuePairs() as $pair) { + if ($pair['key'] instanceof ConstantExpression && ctype_digit((string) $pair['key']->getAttribute('value')) && $pair['key']->getAttribute('value') > $this->index) { + $this->index = $pair['key']->getAttribute('value'); + } + } + } + + public function getKeyValuePairs(): array + { + $pairs = []; + foreach (array_chunk($this->nodes, 2) as $pair) { + $pairs[] = [ + 'key' => $pair[0], + 'value' => $pair[1], + ]; + } + + return $pairs; + } + + public function hasElement(AbstractExpression $key): bool + { + foreach ($this->getKeyValuePairs() as $pair) { + // we compare the string representation of the keys + // to avoid comparing the line numbers which are not relevant here. + if ((string) $key === (string) $pair['key']) { + return true; + } + } + + return false; + } + + /** + * Checks if the array is a sequence (keys are sequential integers starting from 0). + * + * @internal + */ + public function isSequence(): bool + { + foreach ($this->getKeyValuePairs() as $i => $pair) { + $key = $pair['key']; + if ($key instanceof TempNameExpression) { + $keyValue = $key->getAttribute('name'); + } elseif ($key instanceof ConstantExpression) { + $keyValue = $key->getAttribute('value'); + } else { + return false; + } + + if ($keyValue !== $i) { + return false; + } + } + + return true; + } + + public function addElement(AbstractExpression $value, ?AbstractExpression $key = null): void + { + if (null === $key) { + $key = new ConstantExpression(++$this->index, $value->getTemplateLine()); + } + + array_push($this->nodes, $key, $value); + } + + public function getStringCoercedChildNames(): array + { + // dynamic mapping keys (computed at runtime) are coerced to string; + // static keys (constants or sequence indexes) are emitted as PHP + // literals by compile() and never trigger a __toString() call + $names = []; + foreach (array_chunk($this->nodes, 2) as $i => $pair) { + $key = $pair[0]; + if ($key instanceof ConstantExpression || $key instanceof TempNameExpression) { + continue; + } + + $names[] = (string) ($i * 2); + } + + return $names; + } + + public function compile(Compiler $compiler): void + { + if ($this->definedTest) { + $compiler->repr(true); + + return; + } + + // Check for empty expressions which are only allowed in destructuring + foreach ($this->getKeyValuePairs() as $pair) { + if ($pair['value'] instanceof EmptyExpression) { + throw new SyntaxError('Empty array elements are only allowed in destructuring assignments.', $pair['value']->getTemplateLine(), $this->getSourceContext()); + } + } + + $compiler->raw('['); + $isSequence = true; + foreach ($this->getKeyValuePairs() as $i => $pair) { + if (0 !== $i) { + $compiler->raw(', '); + } + + $key = null; + if ($pair['key'] instanceof TempNameExpression) { + $key = $pair['key']->getAttribute('name'); + $pair['key'] = new ConstantExpression($key, $pair['key']->getTemplateLine()); + } elseif ($pair['key'] instanceof ConstantExpression) { + $key = $pair['key']->getAttribute('value'); + } else { + // dynamic key: cast to string so PHP accepts it as an array offset + // (the sandbox visitor has already wrapped it with a __toString policy check) + $pair['key'] = new StringCastUnary($pair['key'], $pair['key']->getTemplateLine()); + } + + if ($key !== $i) { + $isSequence = false; + } + + if (!$isSequence && !$pair['value'] instanceof SpreadUnary) { + $compiler + ->subcompile($pair['key']) + ->raw(' => ') + ; + } + + $compiler->subcompile($pair['value']); + } + $compiler->raw(']'); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/ArrowFunctionExpression.php b/novaconium/vendor/twig/src/Node/Expression/ArrowFunctionExpression.php new file mode 100644 index 0000000..9156f7c --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/ArrowFunctionExpression.php @@ -0,0 +1,63 @@ + + */ +class ArrowFunctionExpression extends AbstractExpression +{ + public function __construct(AbstractExpression $expr, Node $names, $lineno) + { + if ($names instanceof ContextVariable) { + $names = new ListExpression([new AssignContextVariable($names->getAttribute('name'), $names->getTemplateLine())], $lineno); + } + + if (!$names instanceof ListExpression) { + throw new SyntaxError('The arrow function argument must be a list of variables or a single variable.', $names->getTemplateLine(), $names->getSourceContext()); + } + + parent::__construct(['expr' => $expr, 'names' => $names], [], $lineno); + } + + public function compile(Compiler $compiler): void + { + $compiler + ->addDebugInfo($this) + ->raw('function (') + ->subcompile($this->getNode('names')) + ->raw(') use ($context, $macros) { ') + ; + foreach ($this->getNode('names') as $name) { + $compiler + ->raw('$context["') + ->raw($name->getAttribute('name')) + ->raw('"] = $__') + ->raw($name->getAttribute('name')) + ->raw('__; ') + ; + } + $compiler + ->raw('return ') + ->subcompile($this->getNode('expr')) + ->raw('; }') + ; + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/AssignNameExpression.php b/novaconium/vendor/twig/src/Node/Expression/AssignNameExpression.php new file mode 100644 index 0000000..9a7f0f9 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/AssignNameExpression.php @@ -0,0 +1,44 @@ +raw('$context[') + ->string($this->getAttribute('name')) + ->raw(']') + ; + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Binary/AbstractBinary.php b/novaconium/vendor/twig/src/Node/Expression/Binary/AbstractBinary.php new file mode 100644 index 0000000..b4bf666 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Binary/AbstractBinary.php @@ -0,0 +1,53 @@ + $left, 'right' => $right], [], $lineno); + } + + public function compile(Compiler $compiler): void + { + $compiler + ->raw('(') + ->subcompile($this->getNode('left')) + ->raw(' ') + ; + $this->operator($compiler); + $compiler + ->raw(' ') + ->subcompile($this->getNode('right')) + ->raw(')') + ; + } + + abstract public function operator(Compiler $compiler): Compiler; +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Binary/AddBinary.php b/novaconium/vendor/twig/src/Node/Expression/Binary/AddBinary.php new file mode 100644 index 0000000..42377ae --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Binary/AddBinary.php @@ -0,0 +1,24 @@ +raw('+'); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Binary/AndBinary.php b/novaconium/vendor/twig/src/Node/Expression/Binary/AndBinary.php new file mode 100644 index 0000000..7ccef17 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Binary/AndBinary.php @@ -0,0 +1,31 @@ +raw('&&'); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Binary/BinaryInterface.php b/novaconium/vendor/twig/src/Node/Expression/Binary/BinaryInterface.php new file mode 100644 index 0000000..eeeb2eb --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Binary/BinaryInterface.php @@ -0,0 +1,22 @@ +raw('&'); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Binary/BitwiseOrBinary.php b/novaconium/vendor/twig/src/Node/Expression/Binary/BitwiseOrBinary.php new file mode 100644 index 0000000..ec17e22 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Binary/BitwiseOrBinary.php @@ -0,0 +1,24 @@ +raw('|'); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Binary/BitwiseXorBinary.php b/novaconium/vendor/twig/src/Node/Expression/Binary/BitwiseXorBinary.php new file mode 100644 index 0000000..e6432a7 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Binary/BitwiseXorBinary.php @@ -0,0 +1,24 @@ +raw('^'); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Binary/ConcatBinary.php b/novaconium/vendor/twig/src/Node/Expression/Binary/ConcatBinary.php new file mode 100644 index 0000000..e4ec135 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Binary/ConcatBinary.php @@ -0,0 +1,30 @@ +raw('.'); + } + + public function getStringCoercedChildNames(): array + { + return ['left', 'right']; + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Binary/DivBinary.php b/novaconium/vendor/twig/src/Node/Expression/Binary/DivBinary.php new file mode 100644 index 0000000..11c061e --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Binary/DivBinary.php @@ -0,0 +1,24 @@ +raw('/'); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Binary/ElvisBinary.php b/novaconium/vendor/twig/src/Node/Expression/Binary/ElvisBinary.php new file mode 100644 index 0000000..1340a4f --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Binary/ElvisBinary.php @@ -0,0 +1,56 @@ +setNode('test', TrueTest::wrap(clone $left)); + $left->setAttribute('always_defined', true); + } + + public function compile(Compiler $compiler): void + { + $compiler + ->raw('((') + ->subcompile($this->getNode('test')) + ->raw(') ? (') + ->subcompile($this->getNode('left')) + ->raw(') : (') + ->subcompile($this->getNode('right')) + ->raw('))') + ; + } + + public function operator(Compiler $compiler): Compiler + { + return $compiler->raw('?:'); + } + + public function getOperandNamesToEscape(): array + { + return ['left', 'right']; + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Binary/EndsWithBinary.php b/novaconium/vendor/twig/src/Node/Expression/Binary/EndsWithBinary.php new file mode 100644 index 0000000..e689d66 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Binary/EndsWithBinary.php @@ -0,0 +1,36 @@ +getVarName(); + $right = $compiler->getVarName(); + $compiler + ->raw(\sprintf('(is_string($%s = ', $left)) + ->subcompile($this->getNode('left')) + ->raw(\sprintf(') && is_string($%s = ', $right)) + ->subcompile($this->getNode('right')) + ->raw(\sprintf(') && str_ends_with($%1$s, $%2$s))', $left, $right)) + ; + } + + public function operator(Compiler $compiler): Compiler + { + return $compiler->raw(''); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Binary/EqualBinary.php b/novaconium/vendor/twig/src/Node/Expression/Binary/EqualBinary.php new file mode 100644 index 0000000..d438e2b --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Binary/EqualBinary.php @@ -0,0 +1,46 @@ += 80000) { + parent::compile($compiler); + + return; + } + + $compiler + ->raw('(0 === CoreExtension::compare(') + ->subcompile($this->getNode('left')) + ->raw(', ') + ->subcompile($this->getNode('right')) + ->raw('))') + ; + } + + public function operator(Compiler $compiler): Compiler + { + return $compiler->raw('=='); + } + + public function getStringCoercedChildNames(): array + { + return ['left', 'right']; + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Binary/FloorDivBinary.php b/novaconium/vendor/twig/src/Node/Expression/Binary/FloorDivBinary.php new file mode 100644 index 0000000..a60ab3b --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Binary/FloorDivBinary.php @@ -0,0 +1,30 @@ +raw('(int) floor('); + parent::compile($compiler); + $compiler->raw(')'); + } + + public function operator(Compiler $compiler): Compiler + { + return $compiler->raw('/'); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Binary/GreaterBinary.php b/novaconium/vendor/twig/src/Node/Expression/Binary/GreaterBinary.php new file mode 100644 index 0000000..318c301 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Binary/GreaterBinary.php @@ -0,0 +1,46 @@ += 80000) { + parent::compile($compiler); + + return; + } + + $compiler + ->raw('(1 === CoreExtension::compare(') + ->subcompile($this->getNode('left')) + ->raw(', ') + ->subcompile($this->getNode('right')) + ->raw('))') + ; + } + + public function operator(Compiler $compiler): Compiler + { + return $compiler->raw('>'); + } + + public function getStringCoercedChildNames(): array + { + return ['left', 'right']; + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Binary/GreaterEqualBinary.php b/novaconium/vendor/twig/src/Node/Expression/Binary/GreaterEqualBinary.php new file mode 100644 index 0000000..2dacd9f --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Binary/GreaterEqualBinary.php @@ -0,0 +1,46 @@ += 80000) { + parent::compile($compiler); + + return; + } + + $compiler + ->raw('(0 <= CoreExtension::compare(') + ->subcompile($this->getNode('left')) + ->raw(', ') + ->subcompile($this->getNode('right')) + ->raw('))') + ; + } + + public function operator(Compiler $compiler): Compiler + { + return $compiler->raw('>='); + } + + public function getStringCoercedChildNames(): array + { + return ['left', 'right']; + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Binary/HasEveryBinary.php b/novaconium/vendor/twig/src/Node/Expression/Binary/HasEveryBinary.php new file mode 100644 index 0000000..9b10711 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Binary/HasEveryBinary.php @@ -0,0 +1,34 @@ +raw('CoreExtension::arrayEvery($this->env, ') + ->subcompile($this->getNode('left')) + ->raw(', ') + ->subcompile($this->getNode('right')) + ->raw(', $this->env->hasExtension(\Twig\Extension\SandboxExtension::class) && $this->env->getExtension(\Twig\Extension\SandboxExtension::class)->isSandboxed($this->source))') + ; + } + + public function operator(Compiler $compiler): Compiler + { + return $compiler->raw(''); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Binary/HasSomeBinary.php b/novaconium/vendor/twig/src/Node/Expression/Binary/HasSomeBinary.php new file mode 100644 index 0000000..86665a3 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Binary/HasSomeBinary.php @@ -0,0 +1,34 @@ +raw('CoreExtension::arraySome($this->env, ') + ->subcompile($this->getNode('left')) + ->raw(', ') + ->subcompile($this->getNode('right')) + ->raw(', $this->env->hasExtension(\Twig\Extension\SandboxExtension::class) && $this->env->getExtension(\Twig\Extension\SandboxExtension::class)->isSandboxed($this->source))') + ; + } + + public function operator(Compiler $compiler): Compiler + { + return $compiler->raw(''); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Binary/InBinary.php b/novaconium/vendor/twig/src/Node/Expression/Binary/InBinary.php new file mode 100644 index 0000000..2f11a54 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Binary/InBinary.php @@ -0,0 +1,40 @@ +raw('CoreExtension::inFilter(') + ->subcompile($this->getNode('left')) + ->raw(', ') + ->subcompile($this->getNode('right')) + ->raw(')') + ; + } + + public function operator(Compiler $compiler): Compiler + { + return $compiler->raw('in'); + } + + public function getStringCoercedChildNames(): array + { + return ['left', 'right']; + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Binary/LessBinary.php b/novaconium/vendor/twig/src/Node/Expression/Binary/LessBinary.php new file mode 100644 index 0000000..3bb6291 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Binary/LessBinary.php @@ -0,0 +1,46 @@ += 80000) { + parent::compile($compiler); + + return; + } + + $compiler + ->raw('(-1 === CoreExtension::compare(') + ->subcompile($this->getNode('left')) + ->raw(', ') + ->subcompile($this->getNode('right')) + ->raw('))') + ; + } + + public function operator(Compiler $compiler): Compiler + { + return $compiler->raw('<'); + } + + public function getStringCoercedChildNames(): array + { + return ['left', 'right']; + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Binary/LessEqualBinary.php b/novaconium/vendor/twig/src/Node/Expression/Binary/LessEqualBinary.php new file mode 100644 index 0000000..3021745 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Binary/LessEqualBinary.php @@ -0,0 +1,46 @@ += 80000) { + parent::compile($compiler); + + return; + } + + $compiler + ->raw('(0 >= CoreExtension::compare(') + ->subcompile($this->getNode('left')) + ->raw(', ') + ->subcompile($this->getNode('right')) + ->raw('))') + ; + } + + public function operator(Compiler $compiler): Compiler + { + return $compiler->raw('<='); + } + + public function getStringCoercedChildNames(): array + { + return ['left', 'right']; + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Binary/MatchesBinary.php b/novaconium/vendor/twig/src/Node/Expression/Binary/MatchesBinary.php new file mode 100644 index 0000000..5dd3dbe --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Binary/MatchesBinary.php @@ -0,0 +1,66 @@ +getAttribute('value'); + set_error_handler(static fn ($t, $m) => throw new SyntaxError(\sprintf('Regexp "%s" passed to "matches" is not valid: %s.', $regexp, substr($m, 14)), $lineno)); + try { + preg_match($regexp, ''); + } finally { + restore_error_handler(); + } + } + + parent::__construct($left, $right, $lineno); + } + + public function compile(Compiler $compiler): void + { + $compiler + ->raw('CoreExtension::matches(') + ->subcompile($this->getNode('right')) + ->raw(', ') + ->subcompile($this->getNode('left')) + ->raw(')') + ; + } + + public function operator(Compiler $compiler): Compiler + { + return $compiler->raw(''); + } + + public function getStringCoercedChildNames(): array + { + return ['left', 'right']; + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Binary/ModBinary.php b/novaconium/vendor/twig/src/Node/Expression/Binary/ModBinary.php new file mode 100644 index 0000000..aef48f3 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Binary/ModBinary.php @@ -0,0 +1,24 @@ +raw('%'); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Binary/MulBinary.php b/novaconium/vendor/twig/src/Node/Expression/Binary/MulBinary.php new file mode 100644 index 0000000..beb881a --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Binary/MulBinary.php @@ -0,0 +1,24 @@ +raw('*'); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Binary/NotEqualBinary.php b/novaconium/vendor/twig/src/Node/Expression/Binary/NotEqualBinary.php new file mode 100644 index 0000000..c63e325 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Binary/NotEqualBinary.php @@ -0,0 +1,46 @@ += 80000) { + parent::compile($compiler); + + return; + } + + $compiler + ->raw('(0 !== CoreExtension::compare(') + ->subcompile($this->getNode('left')) + ->raw(', ') + ->subcompile($this->getNode('right')) + ->raw('))') + ; + } + + public function operator(Compiler $compiler): Compiler + { + return $compiler->raw('!='); + } + + public function getStringCoercedChildNames(): array + { + return ['left', 'right']; + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Binary/NotInBinary.php b/novaconium/vendor/twig/src/Node/Expression/Binary/NotInBinary.php new file mode 100644 index 0000000..f46c88f --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Binary/NotInBinary.php @@ -0,0 +1,40 @@ +raw('!CoreExtension::inFilter(') + ->subcompile($this->getNode('left')) + ->raw(', ') + ->subcompile($this->getNode('right')) + ->raw(')') + ; + } + + public function operator(Compiler $compiler): Compiler + { + return $compiler->raw('not in'); + } + + public function getStringCoercedChildNames(): array + { + return ['left', 'right']; + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Binary/NotSameAsBinary.php b/novaconium/vendor/twig/src/Node/Expression/Binary/NotSameAsBinary.php new file mode 100644 index 0000000..ed28c19 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Binary/NotSameAsBinary.php @@ -0,0 +1,23 @@ +raw('!=='); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Binary/NullCoalesceBinary.php b/novaconium/vendor/twig/src/Node/Expression/Binary/NullCoalesceBinary.php new file mode 100644 index 0000000..ffba62d --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Binary/NullCoalesceBinary.php @@ -0,0 +1,71 @@ + true]), new EmptyNode(), $left->getTemplateLine()); + // for "block()", we don't need the null test as the return value is always a string + if (!$left instanceof BlockReferenceExpression) { + $test = new AndBinary( + $test, + new NotUnary(new NullTest($left, new TwigTest('null', null, ['always_allowed_in_sandbox' => true]), new EmptyNode(), $left->getTemplateLine()), $left->getTemplateLine()), + $left->getTemplateLine(), + ); + } + + $left->setAttribute('always_defined', true); + $this->setNode('test', $test); + } + + public function compile(Compiler $compiler): void + { + $compiler + ->raw('((') + ->subcompile($this->getNode('test')) + ->raw(') ? (') + ->subcompile($this->getNode('left')) + ->raw(') : (') + ->subcompile($this->getNode('right')) + ->raw('))') + ; + } + + public function operator(Compiler $compiler): Compiler + { + return $compiler->raw('??'); + } + + public function getOperandNamesToEscape(): array + { + return ['left', 'right']; + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Binary/ObjectDestructuringSetBinary.php b/novaconium/vendor/twig/src/Node/Expression/Binary/ObjectDestructuringSetBinary.php new file mode 100644 index 0000000..8c5df6a --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Binary/ObjectDestructuringSetBinary.php @@ -0,0 +1,77 @@ + */ + private array $mappings = []; + + /** + * @param ArrayExpression $left The array expression containing object/mapping destructuring properties + * @param AbstractExpression $right The expression providing values for assignment + */ + public function __construct(Node $left, Node $right, int $lineno) + { + if (!$left instanceof ArrayExpression) { + throw new \LogicException('Left side must be ArrayExpression for object/mapping destructuring.'); + } + foreach ($left->getKeyValuePairs() as $pair) { + if (!$pair['value'] instanceof ContextVariable) { + throw new SyntaxError(\sprintf('Cannot assign to "%s", only variables can be assigned in object/mapping destructuring.', $pair['value']::class), $lineno); + } + + $this->mappings[] = [ + 'property' => $pair['key']->getAttribute('value'), + 'variable' => $pair['value']->getAttribute('name'), + ]; + } + + parent::__construct($left, $right, $lineno); + } + + public function compile(Compiler $compiler): void + { + $compiler->addDebugInfo($this); + $compiler->raw('['); + foreach ($this->mappings as $i => $mapping) { + if ($i) { + $compiler->raw(', '); + } + $compiler->raw('$context[')->repr($mapping['variable'])->raw(']'); + } + $compiler->raw('] = ['); + foreach ($this->mappings as $i => $mapping) { + if ($i) { + $compiler->raw(', '); + } + $compiler->raw('CoreExtension::getAttribute($this->env, $this->source, ')->subcompile($this->getNode('right'))->raw(', ')->repr($mapping['property'])->raw(', [], \\Twig\\Template::ANY_CALL, false, false, ')->repr($compiler->getEnvironment()->hasExtension(SandboxExtension::class))->raw(', ')->repr($this->getNode('right')->getTemplateLine())->raw(')'); + } + $compiler->raw(']'); + } + + public function operator(Compiler $compiler): Compiler + { + return $compiler->raw('='); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Binary/OrBinary.php b/novaconium/vendor/twig/src/Node/Expression/Binary/OrBinary.php new file mode 100644 index 0000000..cacffda --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Binary/OrBinary.php @@ -0,0 +1,31 @@ +raw('||'); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Binary/PowerBinary.php b/novaconium/vendor/twig/src/Node/Expression/Binary/PowerBinary.php new file mode 100644 index 0000000..5325e8e --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Binary/PowerBinary.php @@ -0,0 +1,23 @@ +raw('**'); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Binary/RangeBinary.php b/novaconium/vendor/twig/src/Node/Expression/Binary/RangeBinary.php new file mode 100644 index 0000000..2d0d2eb --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Binary/RangeBinary.php @@ -0,0 +1,40 @@ +raw('range(') + ->subcompile($this->getNode('left')) + ->raw(', ') + ->subcompile($this->getNode('right')) + ->raw(')') + ; + } + + public function operator(Compiler $compiler): Compiler + { + return $compiler->raw('..'); + } + + public function getStringCoercedChildNames(): array + { + return ['left', 'right']; + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Binary/SameAsBinary.php b/novaconium/vendor/twig/src/Node/Expression/Binary/SameAsBinary.php new file mode 100644 index 0000000..b08a190 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Binary/SameAsBinary.php @@ -0,0 +1,23 @@ +raw('==='); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Binary/SequenceDestructuringSetBinary.php b/novaconium/vendor/twig/src/Node/Expression/Binary/SequenceDestructuringSetBinary.php new file mode 100644 index 0000000..a6a873c --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Binary/SequenceDestructuringSetBinary.php @@ -0,0 +1,67 @@ +getKeyValuePairs() as $pair) { + if ($pair['value'] instanceof EmptyExpression) { + $this->variables[] = null; + } elseif ($pair['value'] instanceof ContextVariable) { + $this->variables[] = $pair['value']->getAttribute('name'); + } else { + throw new SyntaxError(\sprintf('Cannot assign to "%s", only variables can be assigned in sequence destructuring.', $pair['value']::class), $lineno); + } + } + + parent::__construct($left, $right, $lineno); + } + + public function compile(Compiler $compiler): void + { + $compiler->addDebugInfo($this); + $compiler->raw('['); + foreach ($this->variables as $i => $name) { + if ($i) { + $compiler->raw(', '); + } + if (null !== $name) { + $compiler->raw('$context[')->repr($name)->raw(']'); + } + } + $compiler->raw('] = array_pad(')->subcompile($this->getNode('right'))->raw(', ')->repr(\count($this->variables))->raw(', null)'); + } + + public function operator(Compiler $compiler): Compiler + { + return $compiler->raw('='); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Binary/SetBinary.php b/novaconium/vendor/twig/src/Node/Expression/Binary/SetBinary.php new file mode 100644 index 0000000..7b34432 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Binary/SetBinary.php @@ -0,0 +1,44 @@ + + */ +class SetBinary extends AbstractBinary +{ + /** + * @param ContextVariable $left + * @param AbstractExpression $right + */ + public function __construct(Node $left, Node $right, int $lineno) + { + $name = $left->getAttribute('name'); + if (!\is_string($name)) { + throw new \LogicException('The "name" attribute must be a string.'); + } + $left = new AssignContextVariable($name, $left->getTemplateLine()); + + parent::__construct($left, $right, $lineno); + } + + public function operator(Compiler $compiler): Compiler + { + return $compiler->raw('='); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Binary/SpaceshipBinary.php b/novaconium/vendor/twig/src/Node/Expression/Binary/SpaceshipBinary.php new file mode 100644 index 0000000..2fb5ddf --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Binary/SpaceshipBinary.php @@ -0,0 +1,29 @@ +raw('<=>'); + } + + public function getStringCoercedChildNames(): array + { + return ['left', 'right']; + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Binary/StartsWithBinary.php b/novaconium/vendor/twig/src/Node/Expression/Binary/StartsWithBinary.php new file mode 100644 index 0000000..ef2fc95 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Binary/StartsWithBinary.php @@ -0,0 +1,36 @@ +getVarName(); + $right = $compiler->getVarName(); + $compiler + ->raw(\sprintf('(is_string($%s = ', $left)) + ->subcompile($this->getNode('left')) + ->raw(\sprintf(') && is_string($%s = ', $right)) + ->subcompile($this->getNode('right')) + ->raw(\sprintf(') && str_starts_with($%1$s, $%2$s))', $left, $right)) + ; + } + + public function operator(Compiler $compiler): Compiler + { + return $compiler->raw(''); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Binary/SubBinary.php b/novaconium/vendor/twig/src/Node/Expression/Binary/SubBinary.php new file mode 100644 index 0000000..10663f5 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Binary/SubBinary.php @@ -0,0 +1,24 @@ +raw('-'); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Binary/XorBinary.php b/novaconium/vendor/twig/src/Node/Expression/Binary/XorBinary.php new file mode 100644 index 0000000..0e9173a --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Binary/XorBinary.php @@ -0,0 +1,31 @@ +raw('xor'); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/BlockReferenceExpression.php b/novaconium/vendor/twig/src/Node/Expression/BlockReferenceExpression.php new file mode 100644 index 0000000..657a637 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/BlockReferenceExpression.php @@ -0,0 +1,102 @@ + + */ +class BlockReferenceExpression extends AbstractExpression implements SupportDefinedTestInterface, CoercesChildrenToStringInterface +{ + use SupportDefinedTestDeprecationTrait; + use SupportDefinedTestTrait; + + /** + * @param AbstractExpression $name + */ + public function __construct(Node $name, ?Node $template, int $lineno) + { + if (!$name instanceof AbstractExpression) { + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, $name::class); + } + + $nodes = ['name' => $name]; + if (null !== $template) { + $nodes['template'] = $template; + } + + parent::__construct($nodes, ['output' => false], $lineno); + } + + public function compile(Compiler $compiler): void + { + if ($this->definedTest) { + $this->compileTemplateCall($compiler, 'hasBlock'); + } else { + if ($this->getAttribute('output')) { + $compiler->addDebugInfo($this); + + $compiler->write('yield from '); + $this + ->compileTemplateCall($compiler, 'yieldBlock') + ->raw(";\n"); + } else { + $this->compileTemplateCall($compiler, 'renderBlock'); + } + } + } + + public function getStringCoercedChildNames(): array + { + // the template expression is resolved through the loader, which coerces it to a string + return $this->hasNode('template') ? ['template'] : []; + } + + private function compileTemplateCall(Compiler $compiler, string $method): Compiler + { + if (!$this->hasNode('template')) { + $compiler->write('$this'); + } else { + $compiler + ->write('$this->load(') + ->subcompile($this->getNode('template')) + ->raw(', ') + ->repr($this->getTemplateLine()) + ->raw(')') + ; + } + + $compiler->raw(\sprintf('->unwrap()->%s', $method)); + + return $this->compileBlockArguments($compiler); + } + + private function compileBlockArguments(Compiler $compiler): Compiler + { + $compiler + ->raw('(') + ->subcompile($this->getNode('name')) + ->raw(', $context'); + + if (!$this->hasNode('template')) { + $compiler->raw(', $blocks'); + } + + return $compiler->raw(')'); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/CallExpression.php b/novaconium/vendor/twig/src/Node/Expression/CallExpression.php new file mode 100644 index 0000000..ad6d388 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/CallExpression.php @@ -0,0 +1,375 @@ +getTwigCallable(); + $callable = $twigCallable->getCallable(); + + if (\is_string($callable) && !str_contains($callable, '::')) { + $compiler->raw($callable); + } else { + $rc = $this->reflectCallable($twigCallable); + $r = $rc->getReflector(); + $callable = $rc->getCallable(); + + if (\is_string($callable)) { + $compiler->raw($callable); + } elseif (\is_array($callable) && \is_string($callable[0])) { + if (!$r instanceof \ReflectionMethod || $r->isStatic()) { + $compiler->raw(\sprintf('%s::%s', $callable[0], $callable[1])); + } else { + $compiler->raw(\sprintf('$this->env->getRuntime(\'%s\')->%s', $callable[0], $callable[1])); + } + } elseif (\is_array($callable) && $callable[0] instanceof ExtensionInterface) { + $class = \get_class($callable[0]); + if (!$compiler->getEnvironment()->hasExtension($class)) { + // Compile a non-optimized call to trigger a \Twig\Error\RuntimeError, which cannot be a compile-time error + $compiler->raw(\sprintf('$this->env->getExtension(\'%s\')', $class)); + } else { + $compiler->raw(\sprintf('$this->extensions[\'%s\']', ltrim($class, '\\'))); + } + + $compiler->raw(\sprintf('->%s', $callable[1])); + } else { + $compiler->raw(\sprintf('$this->env->get%s(\'%s\')->getCallable()', ucfirst($this->getAttribute('type')), $twigCallable->getDynamicName())); + } + } + + $this->compileArguments($compiler); + } + + protected function compileArguments(Compiler $compiler, $isArray = false): void + { + if (\func_num_args() >= 2) { + trigger_deprecation('twig/twig', '3.11', 'Passing a second argument to "%s()" is deprecated.', __METHOD__); + } + + $compiler->raw($isArray ? '[' : '('); + + $first = true; + + $twigCallable = $this->getAttribute('twig_callable'); + + if ($twigCallable->needsCharset()) { + $compiler->raw('$this->env->getCharset()'); + $first = false; + } + + if ($twigCallable->needsEnvironment()) { + if (!$first) { + $compiler->raw(', '); + } + $compiler->raw('$this->env'); + $first = false; + } + + if ($twigCallable->needsContext()) { + if (!$first) { + $compiler->raw(', '); + } + $compiler->raw('$context'); + $first = false; + } + + if (self::needsIsSandboxed($twigCallable)) { + if (!$first) { + $compiler->raw(', '); + } + $compiler->raw('$this->env->hasExtension(\Twig\Extension\SandboxExtension::class) && $this->env->getExtension(\Twig\Extension\SandboxExtension::class)->isSandboxed($this->source)'); + $first = false; + } + + foreach ($twigCallable->getArguments() as $argument) { + if (!$first) { + $compiler->raw(', '); + } + $compiler->string($argument); + $first = false; + } + + if ($this->hasNode('node')) { + if (!$first) { + $compiler->raw(', '); + } + $compiler->subcompile($this->getNode('node')); + $first = false; + } + + if ($this->hasNode('arguments')) { + $arguments = (new CallableArgumentsExtractor($this, $this->getTwigCallable()))->extractArguments($this->getNode('arguments')); + foreach ($arguments as $node) { + if (!$first) { + $compiler->raw(', '); + } + $compiler->subcompile($node); + $first = false; + } + } + + $compiler->raw($isArray ? ']' : ')'); + } + + /** + * @deprecated since Twig 3.12, use Twig\Util\CallableArgumentsExtractor::getArguments() instead + */ + protected function getArguments($callable, $arguments) + { + trigger_deprecation('twig/twig', '3.12', 'The "%s()" method is deprecated, use Twig\Util\CallableArgumentsExtractor::getArguments() instead.', __METHOD__); + + $callType = $this->getAttribute('type'); + $callName = $this->getAttribute('name'); + + $parameters = []; + $named = false; + foreach ($arguments as $name => $node) { + if (!\is_int($name)) { + $named = true; + $name = $this->normalizeName($name); + } elseif ($named) { + throw new SyntaxError(\sprintf('Positional arguments cannot be used after named arguments for %s "%s".', $callType, $callName), $this->getTemplateLine(), $this->getSourceContext()); + } + + $parameters[$name] = $node; + } + + $isVariadic = $this->getAttribute('twig_callable')->isVariadic(); + if (!$named && !$isVariadic) { + return $parameters; + } + + if (!$callable) { + if ($named) { + $message = \sprintf('Named arguments are not supported for %s "%s".', $callType, $callName); + } else { + $message = \sprintf('Arbitrary positional arguments are not supported for %s "%s".', $callType, $callName); + } + + throw new \LogicException($message); + } + + [$callableParameters, $isPhpVariadic] = $this->getCallableParameters($callable, $isVariadic); + $arguments = []; + $names = []; + $missingArguments = []; + $optionalArguments = []; + $pos = 0; + foreach ($callableParameters as $callableParameter) { + $name = $this->normalizeName($callableParameter->name); + if (\PHP_VERSION_ID >= 80000 && 'range' === $callable) { + if ('start' === $name) { + $name = 'low'; + } elseif ('end' === $name) { + $name = 'high'; + } + } + + $names[] = $name; + + if (\array_key_exists($name, $parameters)) { + if (\array_key_exists($pos, $parameters)) { + throw new SyntaxError(\sprintf('Argument "%s" is defined twice for %s "%s".', $name, $callType, $callName), $this->getTemplateLine(), $this->getSourceContext()); + } + + if (\count($missingArguments)) { + throw new SyntaxError(\sprintf( + 'Argument "%s" could not be assigned for %s "%s(%s)" because it is mapped to an internal PHP function which cannot determine default value for optional argument%s "%s".', + $name, $callType, $callName, implode(', ', $names), \count($missingArguments) > 1 ? 's' : '', implode('", "', $missingArguments) + ), $this->getTemplateLine(), $this->getSourceContext()); + } + + $arguments = array_merge($arguments, $optionalArguments); + $arguments[] = $parameters[$name]; + unset($parameters[$name]); + $optionalArguments = []; + } elseif (\array_key_exists($pos, $parameters)) { + $arguments = array_merge($arguments, $optionalArguments); + $arguments[] = $parameters[$pos]; + unset($parameters[$pos]); + $optionalArguments = []; + ++$pos; + } elseif ($callableParameter->isDefaultValueAvailable()) { + $optionalArguments[] = new ConstantExpression($callableParameter->getDefaultValue(), -1); + } elseif ($callableParameter->isOptional()) { + if (!$parameters) { + break; + } + $missingArguments[] = $name; + } else { + throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $name, $callType, $callName), $this->getTemplateLine(), $this->getSourceContext()); + } + } + + if ($isVariadic) { + $arbitraryArguments = $isPhpVariadic ? new VariadicExpression([], -1) : new ArrayExpression([], -1); + foreach ($parameters as $key => $value) { + if (\is_int($key)) { + $arbitraryArguments->addElement($value); + } else { + $arbitraryArguments->addElement($value, new ConstantExpression($key, -1)); + } + unset($parameters[$key]); + } + + if ($arbitraryArguments->count()) { + $arguments = array_merge($arguments, $optionalArguments); + $arguments[] = $arbitraryArguments; + } + } + + if ($parameters) { + $unknownParameter = null; + foreach ($parameters as $parameter) { + if ($parameter instanceof Node) { + $unknownParameter = $parameter; + break; + } + } + + throw new SyntaxError( + \sprintf( + 'Unknown argument%s "%s" for %s "%s(%s)".', + \count($parameters) > 1 ? 's' : '', implode('", "', array_keys($parameters)), $callType, $callName, implode(', ', $names) + ), + $unknownParameter ? $unknownParameter->getTemplateLine() : $this->getTemplateLine(), + $unknownParameter ? $unknownParameter->getSourceContext() : $this->getSourceContext() + ); + } + + return $arguments; + } + + /** + * @deprecated since Twig 3.12 + */ + protected function normalizeName(string $name): string + { + trigger_deprecation('twig/twig', '3.12', 'The "%s()" method is deprecated.', __METHOD__); + + return strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z\d])([A-Z])/'], ['\\1_\\2', '\\1_\\2'], $name)); + } + + // To be removed in 4.0 + private function getCallableParameters($callable, bool $isVariadic): array + { + $twigCallable = $this->getAttribute('twig_callable'); + $rc = $this->reflectCallable($twigCallable); + $callableName = $rc->getName(); + + $parameters = $rc->getTwigParameters($this->hasNode('node')); + + $isPhpVariadic = false; + if ($isVariadic) { + $argument = end($parameters); + $isArray = $argument && $argument->hasType() && $argument->getType() instanceof \ReflectionNamedType && 'array' === $argument->getType()->getName(); + if ($isArray && $argument->isDefaultValueAvailable() && [] === $argument->getDefaultValue()) { + array_pop($parameters); + } elseif ($argument && $argument->isVariadic()) { + array_pop($parameters); + $isPhpVariadic = true; + } else { + throw new \LogicException(\sprintf('The last parameter of "%s" for %s "%s" must be an array with default value, eg. "array $arg = []".', $callableName, $this->getAttribute('type'), $twigCallable->getName())); + } + } + + return [$parameters, $isPhpVariadic]; + } + + private function reflectCallable(TwigCallableInterface $callable): ReflectionCallable + { + if (!$this->reflector) { + $this->reflector = new ReflectionCallable($callable); + } + + return $this->reflector; + } + + /** + * @internal + * + * To be removed in 4.0 and replaced by $twigCallable->needsIsSandboxed(). + */ + public static function needsIsSandboxed(TwigCallableInterface $twigCallable): bool + { + if (method_exists($twigCallable, 'needsIsSandboxed')) { + return $twigCallable->needsIsSandboxed(); + } + + trigger_deprecation('twig/twig', '3.25', 'Not implementing the "needsIsSandboxed()" method in "%s" is deprecated. This method will be part of the "%s" interface in 4.0.', $twigCallable::class, TwigCallableInterface::class); + + return false; + } + + /** + * Overrides the Twig callable based on attributes (as potentially, attributes changed between the creation and the compilation of the node). + * + * To be removed in 4.0 and replace by $this->getAttribute('twig_callable'). + */ + private function getTwigCallable(): TwigCallableInterface + { + $current = $this->getAttribute('twig_callable'); + + $this->setAttribute('twig_callable', match ($this->getAttribute('type')) { + 'test' => (new TwigTest( + $this->getAttribute('name'), + $this->hasAttribute('callable') ? $this->getAttribute('callable') : $current->getCallable(), + [ + 'needs_is_sandboxed' => $this->hasAttribute('needs_is_sandboxed') ? $this->getAttribute('needs_is_sandboxed') : self::needsIsSandboxed($current), + 'is_variadic' => $this->hasAttribute('is_variadic') ? $this->getAttribute('is_variadic') : $current->isVariadic(), + ], + ))->withDynamicArguments($this->getAttribute('name'), $this->hasAttribute('dynamic_name') ? $this->getAttribute('dynamic_name') : $current->getDynamicName(), $this->hasAttribute('arguments') ? $this->getAttribute('arguments') : $current->getArguments()), + 'function' => (new TwigFunction( + $this->hasAttribute('name') ? $this->getAttribute('name') : $current->getName(), + $this->hasAttribute('callable') ? $this->getAttribute('callable') : $current->getCallable(), + [ + 'needs_environment' => $this->hasAttribute('needs_environment') ? $this->getAttribute('needs_environment') : $current->needsEnvironment(), + 'needs_context' => $this->hasAttribute('needs_context') ? $this->getAttribute('needs_context') : $current->needsContext(), + 'needs_charset' => $this->hasAttribute('needs_charset') ? $this->getAttribute('needs_charset') : $current->needsCharset(), + 'needs_is_sandboxed' => $this->hasAttribute('needs_is_sandboxed') ? $this->getAttribute('needs_is_sandboxed') : self::needsIsSandboxed($current), + 'is_variadic' => $this->hasAttribute('is_variadic') ? $this->getAttribute('is_variadic') : $current->isVariadic(), + ], + ))->withDynamicArguments($this->getAttribute('name'), $this->hasAttribute('dynamic_name') ? $this->getAttribute('dynamic_name') : $current->getDynamicName(), $this->hasAttribute('arguments') ? $this->getAttribute('arguments') : $current->getArguments()), + 'filter' => (new TwigFilter( + $this->getAttribute('name'), + $this->hasAttribute('callable') ? $this->getAttribute('callable') : $current->getCallable(), + [ + 'needs_environment' => $this->hasAttribute('needs_environment') ? $this->getAttribute('needs_environment') : $current->needsEnvironment(), + 'needs_context' => $this->hasAttribute('needs_context') ? $this->getAttribute('needs_context') : $current->needsContext(), + 'needs_charset' => $this->hasAttribute('needs_charset') ? $this->getAttribute('needs_charset') : $current->needsCharset(), + 'needs_is_sandboxed' => $this->hasAttribute('needs_is_sandboxed') ? $this->getAttribute('needs_is_sandboxed') : self::needsIsSandboxed($current), + 'is_variadic' => $this->hasAttribute('is_variadic') ? $this->getAttribute('is_variadic') : $current->isVariadic(), + ], + ))->withDynamicArguments($this->getAttribute('name'), $this->hasAttribute('dynamic_name') ? $this->getAttribute('dynamic_name') : $current->getDynamicName(), $this->hasAttribute('arguments') ? $this->getAttribute('arguments') : $current->getArguments()), + }); + + return $this->getAttribute('twig_callable'); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/ConditionalExpression.php b/novaconium/vendor/twig/src/Node/Expression/ConditionalExpression.php new file mode 100644 index 0000000..7fe309c --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/ConditionalExpression.php @@ -0,0 +1,53 @@ + $expr1, 'expr2' => $expr2, 'expr3' => $expr3], [], $lineno); + } + + public function compile(Compiler $compiler): void + { + // Ternary with no then uses Elvis operator + if ($this->getNode('expr1') === $this->getNode('expr2')) { + $compiler + ->raw('((') + ->subcompile($this->getNode('expr1')) + ->raw(') ?: (') + ->subcompile($this->getNode('expr3')) + ->raw('))'); + } else { + $compiler + ->raw('((') + ->subcompile($this->getNode('expr1')) + ->raw(') ? (') + ->subcompile($this->getNode('expr2')) + ->raw(') : (') + ->subcompile($this->getNode('expr3')) + ->raw('))'); + } + } + + public function getOperandNamesToEscape(): array + { + return ['expr2', 'expr3']; + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/ConstantExpression.php b/novaconium/vendor/twig/src/Node/Expression/ConstantExpression.php new file mode 100644 index 0000000..12dc062 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/ConstantExpression.php @@ -0,0 +1,33 @@ + $value], $lineno); + } + + public function compile(Compiler $compiler): void + { + $compiler->repr($this->definedTest ? true : $this->getAttribute('value')); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/EmptyExpression.php b/novaconium/vendor/twig/src/Node/Expression/EmptyExpression.php new file mode 100644 index 0000000..768d1d6 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/EmptyExpression.php @@ -0,0 +1,33 @@ + + */ +class DefaultFilter extends FilterExpression +{ + /** + * @param AbstractExpression $node + */ + #[FirstClassTwigCallableReady] + public function __construct(Node $node, TwigFilter|ConstantExpression $filter, Node $arguments, int $lineno) + { + if (!$node instanceof AbstractExpression) { + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, $node::class); + } + + if ($filter instanceof TwigFilter) { + $name = $filter->getName(); + $default = new FilterExpression($node, $filter, $arguments, $node->getTemplateLine()); + } else { + $name = $filter->getAttribute('value'); + $default = new FilterExpression($node, new TwigFilter('default', [CoreExtension::class, 'default']), $arguments, $node->getTemplateLine()); + } + + if ('default' === $name && ($node instanceof ContextVariable || $node instanceof GetAttrExpression)) { + $test = new DefinedTest(clone $node, new TwigTest('defined', null, ['always_allowed_in_sandbox' => true]), new EmptyNode(), $node->getTemplateLine()); + $false = \count($arguments) ? $arguments->getNode('0') : new ConstantExpression('', $node->getTemplateLine()); + + $node = new ConditionalTernary($test, $default, $false, $node->getTemplateLine()); + } else { + $node = $default; + } + + parent::__construct($node, $filter, $arguments, $lineno); + } + + public function compile(Compiler $compiler): void + { + $compiler->subcompile($this->getNode('node')); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Filter/RawFilter.php b/novaconium/vendor/twig/src/Node/Expression/Filter/RawFilter.php new file mode 100644 index 0000000..707e8ec --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Filter/RawFilter.php @@ -0,0 +1,45 @@ + + */ +class RawFilter extends FilterExpression +{ + /** + * @param AbstractExpression $node + */ + #[FirstClassTwigCallableReady] + public function __construct(Node $node, TwigFilter|ConstantExpression|null $filter = null, ?Node $arguments = null, int $lineno = 0) + { + if (!$node instanceof AbstractExpression) { + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, $node::class); + } + + parent::__construct($node, $filter ?: new TwigFilter('raw', null, ['is_safe' => ['all']]), $arguments ?: new EmptyNode(), $lineno ?: $node->getTemplateLine()); + } + + public function compile(Compiler $compiler): void + { + $compiler->subcompile($this->getNode('node')); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/FilterExpression.php b/novaconium/vendor/twig/src/Node/Expression/FilterExpression.php new file mode 100644 index 0000000..eb94720 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/FilterExpression.php @@ -0,0 +1,87 @@ +getName(); + $filterName = new ConstantExpression($name, $lineno); + } else { + $name = $filter->getAttribute('value'); + $filterName = $filter; + trigger_deprecation('twig/twig', '3.12', 'Not passing an instance of "TwigFilter" when creating a "%s" filter of type "%s" is deprecated.', $name, static::class); + } + + parent::__construct(['node' => $node, 'filter' => $filterName, 'arguments' => $arguments], ['name' => $name, 'type' => 'filter'], $lineno); + + if ($filter instanceof TwigFilter) { + $this->setAttribute('twig_callable', $filter); + } + + $this->deprecateNode('filter', new NameDeprecation('twig/twig', '3.12')); + + $this->deprecateAttribute('needs_charset', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('needs_environment', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('needs_context', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('arguments', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('callable', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('is_variadic', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('dynamic_name', new NameDeprecation('twig/twig', '3.12')); + } + + public function compile(Compiler $compiler): void + { + $name = $this->getNode('filter', false)->getAttribute('value'); + if ($name !== $this->getAttribute('name')) { + trigger_deprecation('twig/twig', '3.11', 'Changing the value of a "filter" node in a NodeVisitor class is not supported anymore.'); + $this->removeAttribute('twig_callable'); + } + if ('raw' === $name) { + trigger_deprecation('twig/twig', '3.11', 'Creating the "raw" filter via "FilterExpression" is deprecated; use "RawFilter" instead.'); + + $compiler->subcompile($this->getNode('node')); + + return; + } + + if (!$this->hasAttribute('twig_callable')) { + $this->setAttribute('twig_callable', $compiler->getEnvironment()->getFilter($name)); + } + + $this->compileCallable($compiler); + } + + public function getStringCoercedChildNames(): array + { + // a filter may coerce its input and arguments to string (e.g. `upper`, `replace`) + return ['node', 'arguments']; + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/FunctionExpression.php b/novaconium/vendor/twig/src/Node/Expression/FunctionExpression.php new file mode 100644 index 0000000..5983962 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/FunctionExpression.php @@ -0,0 +1,88 @@ +getName(); + } else { + $name = $function; + trigger_deprecation('twig/twig', '3.12', 'Not passing an instance of "TwigFunction" when creating a "%s" function of type "%s" is deprecated.', $name, static::class); + } + + parent::__construct(['arguments' => $arguments], ['name' => $name, 'type' => 'function'], $lineno); + + if ($function instanceof TwigFunction) { + $this->setAttribute('twig_callable', $function); + } + + $this->deprecateAttribute('needs_charset', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('needs_environment', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('needs_context', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('arguments', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('callable', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('is_variadic', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('dynamic_name', new NameDeprecation('twig/twig', '3.12')); + } + + public function enableDefinedTest(): void + { + if ('constant' === $this->getAttribute('name')) { + $this->definedTest = true; + } + } + + /** + * @return void + */ + public function compile(Compiler $compiler) + { + $name = $this->getAttribute('name'); + if ($this->hasAttribute('twig_callable')) { + $name = $this->getAttribute('twig_callable')->getName(); + if ($name !== $this->getAttribute('name')) { + trigger_deprecation('twig/twig', '3.12', 'Changing the value of a "function" node in a NodeVisitor class is not supported anymore.'); + $this->removeAttribute('twig_callable'); + } + } + + if (!$this->hasAttribute('twig_callable')) { + $this->setAttribute('twig_callable', $compiler->getEnvironment()->getFunction($name)); + } + + if ('constant' === $name && $this->isDefinedTestEnabled()) { + $this->getNode('arguments')->setNode('checkDefined', new ConstantExpression(true, $this->getTemplateLine())); + } + + $this->compileCallable($compiler); + } + + public function getStringCoercedChildNames(): array + { + // a function may coerce its arguments to string (the host PHP code is opaque to Twig) + return ['arguments']; + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/FunctionNode/EnumCasesFunction.php b/novaconium/vendor/twig/src/Node/Expression/FunctionNode/EnumCasesFunction.php new file mode 100644 index 0000000..170d0a1 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/FunctionNode/EnumCasesFunction.php @@ -0,0 +1,50 @@ +getNode('arguments'); + if ($arguments->hasNode('enum')) { + $firstArgument = $arguments->getNode('enum'); + } elseif ($arguments->hasNode('0')) { + $firstArgument = $arguments->getNode('0'); + } else { + $firstArgument = null; + } + + if (!$firstArgument instanceof ConstantExpression || 1 !== \count($arguments)) { + parent::compile($compiler); + + return; + } + + $value = $firstArgument->getAttribute('value'); + + if (!\is_string($value)) { + throw new SyntaxError('The first argument of the "enum_cases" function must be a string.', $this->getTemplateLine(), $this->getSourceContext()); + } + + if (!enum_exists($value)) { + throw new SyntaxError(\sprintf('The first argument of the "enum_cases" function must be the name of an enum, "%s" given.', $value), $this->getTemplateLine(), $this->getSourceContext()); + } + + $compiler->raw(\sprintf('%s::cases()', $value)); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/FunctionNode/EnumFunction.php b/novaconium/vendor/twig/src/Node/Expression/FunctionNode/EnumFunction.php new file mode 100644 index 0000000..af15b64 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/FunctionNode/EnumFunction.php @@ -0,0 +1,54 @@ +getNode('arguments'); + if ($arguments->hasNode('enum')) { + $firstArgument = $arguments->getNode('enum'); + } elseif ($arguments->hasNode('0')) { + $firstArgument = $arguments->getNode('0'); + } else { + $firstArgument = null; + } + + if (!$firstArgument instanceof ConstantExpression || 1 !== \count($arguments)) { + parent::compile($compiler); + + return; + } + + $value = $firstArgument->getAttribute('value'); + + if (!\is_string($value)) { + throw new SyntaxError('The first argument of the "enum" function must be a string.', $this->getTemplateLine(), $this->getSourceContext()); + } + + if (!enum_exists($value)) { + throw new SyntaxError(\sprintf('The first argument of the "enum" function must be the name of an enum, "%s" given.', $value), $this->getTemplateLine(), $this->getSourceContext()); + } + + if (!$cases = $value::cases()) { + throw new SyntaxError(\sprintf('The first argument of the "enum" function must be a non-empty enum, "%s" given.', $value), $this->getTemplateLine(), $this->getSourceContext()); + } + + $compiler->raw(\sprintf('%s::%s', $value, $cases[0]->name)); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/GetAttrExpression.php b/novaconium/vendor/twig/src/Node/Expression/GetAttrExpression.php new file mode 100644 index 0000000..3dcd88a --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/GetAttrExpression.php @@ -0,0 +1,228 @@ + $node, 'attribute' => $attribute]; + if (null !== $arguments) { + $nodes['arguments'] = $arguments; + } + + if ($arguments && !$arguments instanceof ArrayExpression && !$arguments instanceof ContextVariable) { + trigger_deprecation('twig/twig', '3.15', \sprintf('Not passing a "%s" instance as the "arguments" argument of the "%s" constructor is deprecated ("%s" given).', ArrayExpression::class, static::class, $arguments::class)); + } + + parent::__construct($nodes, ['type' => $type, 'ignore_strict_check' => false, 'optimizable' => !$nullSafe, 'null_safe' => $nullSafe, 'is_short_circuited' => false, 'var_name' => null], $lineno); + } + + public function enableDefinedTest(): void + { + $this->definedTest = true; + $this->changeIgnoreStrictCheck($this); + } + + public function compile(Compiler $compiler): void + { + $env = $compiler->getEnvironment(); + $arrayAccessSandbox = false; + $nullSafe = $this->getAttribute('null_safe'); + + // optimize array calls + if ( + $this->getAttribute('optimizable') + && (!$env->isStrictVariables() || $this->getAttribute('ignore_strict_check')) + && !$this->definedTest + && Template::ARRAY_CALL === $this->getAttribute('type') + ) { + $var = '$'.$compiler->getVarName(); + $compiler + ->raw('(('.$var.' = ') + ->subcompile($this->getNode('node')) + ->raw(') && is_array(') + ->raw($var); + + if (!$env->hasExtension(SandboxExtension::class)) { + $compiler + ->raw(') || ') + ->raw($var) + ->raw(' instanceof ArrayAccess ? (') + ->raw($var) + ->raw('[') + ; + $this->compileArrayKey($compiler); + $compiler->raw('] ?? null) : null)'); + + return; + } + + $arrayAccessSandbox = true; + + $compiler + ->raw(') || ') + ->raw($var) + ->raw(' instanceof ArrayAccess && in_array(') + ->raw($var.'::class') + ->raw(', CoreExtension::ARRAY_LIKE_CLASSES, true) ? (') + ->raw($var) + ->raw('[') + ; + $this->compileArrayKey($compiler); + $compiler->raw('] ?? null) : '); + } + + if ($this->getAttribute('ignore_strict_check')) { + $this->getNode('node')->setAttribute('ignore_strict_check', true); + } + + if (null === $nullSafeNode = $nullSafe ? $this : null) { + $node = $this->getNode('node'); + while ($node instanceof self) { + if ($node->getAttribute('null_safe')) { + $nullSafeNode = $node; + break; + } + $node = $node->getNode('node'); + } + } + + $isShortCircuited = false; + if (null !== $nullSafeNode && !$nullSafeNode->isShortCircuited()) { + $compiler + ->raw('((null === ('.$nullSafeNode->getVarName($compiler).' = ') + ->subcompile($nullSafeNode->getNode('node')) + ->raw(')) ? null : '); + + $nullSafeNode->markAsShortCircuited(); + $isShortCircuited = true; + } + + $compiler->raw('CoreExtension::getAttribute($this->env, $this->source, '); + + if ($nullSafe) { + $compiler->raw($this->getVarName($compiler)); + } else { + $compiler->subcompile($this->getNode('node')); + } + + $compiler + ->raw(', ') + ->subcompile($this->getNode('attribute')) + ; + + if ($this->hasNode('arguments')) { + $compiler->raw(', ')->subcompile($this->getNode('arguments')); + } else { + $compiler->raw(', []'); + } + + $compiler->raw(', ') + ->repr($this->getAttribute('type')) + ->raw(', ')->repr($this->definedTest) + ->raw(', ')->repr($this->getAttribute('ignore_strict_check')) + ->raw(', ')->repr($env->hasExtension(SandboxExtension::class)) + ->raw(', ')->repr($this->getNode('node')->getTemplateLine()) + ->raw(')') + ; + + if ($arrayAccessSandbox) { + $compiler->raw(')'); + } + + if ($isShortCircuited) { + $compiler->raw(')'); + } + } + + public function getStringCoercedChildNames(): array + { + $names = []; + + // the host PHP method may coerce any argument to string + if ($this->hasNode('arguments')) { + $names[] = 'arguments'; + } + + // compileArrayKey() coerces a Stringable key; expose it so the sandbox checks __toString() + if (Template::ARRAY_CALL === $this->getAttribute('type')) { + $names[] = 'attribute'; + } + + return $names; + } + + /** + * Coerces a Stringable array key to string so the optimized path matches + * CoreExtension::getAttribute(); scalars are left to PHP's native offset coercion. + */ + private function compileArrayKey(Compiler $compiler): void + { + $attribute = $this->getNode('attribute'); + + if ($attribute instanceof ConstantExpression) { + $compiler->subcompile($attribute); + + return; + } + + $key = '$'.$compiler->getVarName(); + $compiler + ->raw('(('.$key.' = ') + ->subcompile($attribute) + ->raw(') instanceof \Stringable ? (string) '.$key.' : '.$key.')') + ; + } + + private function changeIgnoreStrictCheck(self $node): void + { + $node->setAttribute('optimizable', false); + $node->setAttribute('ignore_strict_check', true); + + if ($node->getNode('node') instanceof self) { + $this->changeIgnoreStrictCheck($node->getNode('node')); + } + } + + private function markAsShortCircuited(): void + { + $this->setAttribute('is_short_circuited', true); + } + + private function isShortCircuited(): bool + { + return $this->getAttribute('is_short_circuited'); + } + + private function getVarName(Compiler $compiler): string + { + if (null === $this->getAttribute('var_name')) { + $this->setAttribute('var_name', $compiler->getVarName()); + } + + return '$'.$this->getAttribute('var_name'); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/InlinePrint.php b/novaconium/vendor/twig/src/Node/Expression/InlinePrint.php new file mode 100644 index 0000000..5509f79 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/InlinePrint.php @@ -0,0 +1,39 @@ + $node], [], $lineno); + } + + public function compile(Compiler $compiler): void + { + $compiler + ->raw('yield ') + ->subcompile($this->getNode('node')) + ; + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/ListExpression.php b/novaconium/vendor/twig/src/Node/Expression/ListExpression.php new file mode 100644 index 0000000..9b23857 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/ListExpression.php @@ -0,0 +1,41 @@ + $items + */ + public function __construct(array $items, int $lineno) + { + parent::__construct($items, [], $lineno); + } + + public function compile(Compiler $compiler): void + { + foreach ($this as $i => $name) { + if ($i) { + $compiler->raw(', '); + } + + $compiler + ->raw('$__') + ->raw($name->getAttribute('name')) + ->raw('__') + ; + } + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/MacroReferenceExpression.php b/novaconium/vendor/twig/src/Node/Expression/MacroReferenceExpression.php new file mode 100644 index 0000000..fc7a7af --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/MacroReferenceExpression.php @@ -0,0 +1,136 @@ + + */ +class MacroReferenceExpression extends AbstractExpression implements SupportDefinedTestInterface, CoercesChildrenToStringInterface +{ + use SupportDefinedTestDeprecationTrait; + use SupportDefinedTestTrait; + + /** + * @param string|AbstractExpression $name A static macro method name (e.g. "macro_foo") or, for a dynamic + * call, an expression resolving to the macro name (without the + * "macro_" prefix, which is added at runtime) + */ + public function __construct(TemplateVariable $template, string|AbstractExpression $name, AbstractExpression $arguments, int $lineno) + { + $nodes = ['template' => $template, 'arguments' => $arguments]; + $attributes = ['name' => null]; + + if (\is_string($name)) { + // The name is emitted as raw PHP in compile() via "->{$name}(...)", + // so it must be a valid PHP method identifier. Reject anything else + // as a defense-in-depth against accidental PHP code injection from + // a caller that forgot to validate user-controlled input. + if (!preg_match('#^[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*$#D', $name)) { + throw new \LogicException(\sprintf('Macro name "%s" is not a valid PHP identifier.', $name)); + } + $attributes['name'] = $name; + } else { + $nodes['name'] = $name; + } + + parent::__construct($nodes, $attributes, $lineno); + } + + public function __clone() + { + // The template node must not be deep-cloned because its name is + // lazily generated during compilation and must stay in sync with + // the AssignTemplateVariable that populates the $macros array. + $template = $this->nodes['template']; + parent::__clone(); + $this->nodes['template'] = $template; + } + + public function compile(Compiler $compiler): void + { + if ($this->hasNode('name')) { + $this->compileDynamic($compiler); + + return; + } + + if ($this->definedTest) { + $compiler + ->subcompile($this->getNode('template')) + ->raw('->hasMacro(') + ->repr($this->getAttribute('name')) + ->raw(', $context') + ->raw(')') + ; + + return; + } + + $compiler + ->subcompile($this->getNode('template')) + ->raw('->getTemplateForMacro(') + ->repr($this->getAttribute('name')) + ->raw(', $context, ') + ->repr($this->getTemplateLine()) + ->raw(', $this->getSourceContext())') + ->raw(\sprintf('->%s', $this->getAttribute('name'))) + ->raw('(...') + ->subcompile($this->getNode('arguments')) + ->raw(')') + ; + } + + public function getStringCoercedChildNames(): array + { + // Dynamic macro names are prefixed via PHP string concatenation at runtime. + return $this->hasNode('name') ? ['name'] : []; + } + + private function compileDynamic(Compiler $compiler): void + { + // The macro method name is resolved at runtime from a context value; + // prefixing it with "macro_" constrains the dynamic method call to the + // template's macro methods only, and getTemplateForMacro()/hasMacro() + // validate that the method actually exists. + $var = $compiler->getVarName(); + + if ($this->definedTest) { + $compiler + ->subcompile($this->getNode('template')) + ->raw('->hasMacro(\'macro_\'.') + ->subcompile($this->getNode('name')) + ->raw(', $context)') + ; + + return; + } + + $compiler + ->subcompile($this->getNode('template')) + ->raw(\sprintf('->getTemplateForMacro($%s = \'macro_\'.', $var)) + ->subcompile($this->getNode('name')) + ->raw(', $context, ') + ->repr($this->getTemplateLine()) + ->raw(', $this->getSourceContext())') + ->raw(\sprintf('->{$%s}', $var)) + ->raw('(...') + ->subcompile($this->getNode('arguments')) + ->raw(')') + ; + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/MethodCallExpression.php b/novaconium/vendor/twig/src/Node/Expression/MethodCallExpression.php new file mode 100644 index 0000000..4b18053 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/MethodCallExpression.php @@ -0,0 +1,58 @@ + $node, 'arguments' => $arguments], ['method' => $method, 'safe' => false], $lineno); + + if ($node instanceof ContextVariable) { + $node->setAttribute('always_defined', true); + } + } + + public function compile(Compiler $compiler): void + { + if ($this->definedTest) { + $compiler + ->raw('method_exists($macros[') + ->repr($this->getNode('node')->getAttribute('name')) + ->raw('], ') + ->repr($this->getAttribute('method')) + ->raw(')') + ; + + return; + } + + $compiler + ->raw('CoreExtension::callMacro($macros[') + ->repr($this->getNode('node')->getAttribute('name')) + ->raw('], ') + ->repr($this->getAttribute('method')) + ->raw(', ') + ->subcompile($this->getNode('arguments')) + ->raw(', ') + ->repr($this->getTemplateLine()) + ->raw(', $context, $this->getSourceContext())'); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/NameExpression.php b/novaconium/vendor/twig/src/Node/Expression/NameExpression.php new file mode 100644 index 0000000..0e03674 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/NameExpression.php @@ -0,0 +1,115 @@ + '$this->getTemplateName()', + '_context' => '$context', + '_charset' => '$this->env->getCharset()', + ]; + + public function __construct(string $name, int $lineno) + { + if (self::class === static::class) { + trigger_deprecation('twig/twig', '3.15', 'The "%s" class is deprecated, use "%s" instead.', self::class, ContextVariable::class); + } + + parent::__construct([], ['name' => $name, 'ignore_strict_check' => false, 'always_defined' => false], $lineno); + } + + public function compile(Compiler $compiler): void + { + $name = $this->getAttribute('name'); + + $compiler->addDebugInfo($this); + + if ($this->definedTest) { + if (isset($this->specialVars[$name]) || $this->getAttribute('always_defined')) { + $compiler->repr(true); + } elseif (\PHP_VERSION_ID >= 70400) { + $compiler + ->raw('array_key_exists(') + ->string($name) + ->raw(', $context)') + ; + } else { + $compiler + ->raw('(isset($context[') + ->string($name) + ->raw(']) || array_key_exists(') + ->string($name) + ->raw(', $context))') + ; + } + } elseif (isset($this->specialVars[$name])) { + $compiler->raw($this->specialVars[$name]); + } elseif ($this->getAttribute('always_defined')) { + $compiler + ->raw('$context[') + ->string($name) + ->raw(']') + ; + } else { + if ($this->getAttribute('ignore_strict_check') || !$compiler->getEnvironment()->isStrictVariables()) { + $compiler + ->raw('($context[') + ->string($name) + ->raw('] ?? null)') + ; + } else { + $compiler + ->raw('(isset($context[') + ->string($name) + ->raw(']) || array_key_exists(') + ->string($name) + ->raw(', $context) ? $context[') + ->string($name) + ->raw('] : (function () { throw new RuntimeError(\'Variable ') + ->string($name) + ->raw(' does not exist.\', ') + ->repr($this->lineno) + ->raw(', $this->source); })()') + ->raw(')') + ; + } + } + } + + /** + * @deprecated since Twig 3.11 (to be removed in 4.0) + */ + public function isSpecial() + { + trigger_deprecation('twig/twig', '3.11', 'The "%s()" method is deprecated and will be removed in Twig 4.0.', __METHOD__); + + return isset($this->specialVars[$this->getAttribute('name')]); + } + + /** + * @deprecated since Twig 3.11 (to be removed in 4.0) + */ + public function isSimple() + { + trigger_deprecation('twig/twig', '3.11', 'The "%s()" method is deprecated and will be removed in Twig 4.0.', __METHOD__); + + return !isset($this->specialVars[$this->getAttribute('name')]) && !$this->definedTest; + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/NullCoalesceExpression.php b/novaconium/vendor/twig/src/Node/Expression/NullCoalesceExpression.php new file mode 100644 index 0000000..4548519 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/NullCoalesceExpression.php @@ -0,0 +1,77 @@ + true]), new EmptyNode(), $left->getTemplateLine()); + // for "block()", we don't need the null test as the return value is always a string + if (!$left instanceof BlockReferenceExpression) { + $test = new AndBinary( + $test, + new NotUnary(new NullTest($left, new TwigTest('null', null, ['always_allowed_in_sandbox' => true]), new EmptyNode(), $left->getTemplateLine()), $left->getTemplateLine()), + $left->getTemplateLine() + ); + } + + parent::__construct($test, $left, $right, $lineno); + } + + public function compile(Compiler $compiler): void + { + /* + * This optimizes only one case. PHP 7 also supports more complex expressions + * that can return null. So, for instance, if log is defined, log("foo") ?? "..." works, + * but log($a["foo"]) ?? "..." does not if $a["foo"] is not defined. More advanced + * cases might be implemented as an optimizer node visitor, but has not been done + * as benefits are probably not worth the added complexity. + */ + if ($this->getNode('expr2') instanceof ContextVariable) { + $this->getNode('expr2')->setAttribute('always_defined', true); + $compiler + ->raw('((') + ->subcompile($this->getNode('expr2')) + ->raw(') ?? (') + ->subcompile($this->getNode('expr3')) + ->raw('))') + ; + } else { + parent::compile($compiler); + } + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/OperatorEscapeInterface.php b/novaconium/vendor/twig/src/Node/Expression/OperatorEscapeInterface.php new file mode 100644 index 0000000..06db6c6 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/OperatorEscapeInterface.php @@ -0,0 +1,25 @@ + 1. + * + * @author Fabien Potencier + */ +interface OperatorEscapeInterface +{ + /** + * @return string[] + */ + public function getOperandNamesToEscape(): array; +} diff --git a/novaconium/vendor/twig/src/Node/Expression/ParentExpression.php b/novaconium/vendor/twig/src/Node/Expression/ParentExpression.php new file mode 100644 index 0000000..22fe38f --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/ParentExpression.php @@ -0,0 +1,46 @@ + + */ +class ParentExpression extends AbstractExpression +{ + public function __construct(string $name, int $lineno) + { + parent::__construct([], ['output' => false, 'name' => $name], $lineno); + } + + public function compile(Compiler $compiler): void + { + if ($this->getAttribute('output')) { + $compiler + ->addDebugInfo($this) + ->write('yield from $this->yieldParentBlock(') + ->string($this->getAttribute('name')) + ->raw(", \$context, \$blocks);\n") + ; + } else { + $compiler + ->raw('$this->renderParentBlock(') + ->string($this->getAttribute('name')) + ->raw(', $context, $blocks)') + ; + } + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/ReturnArrayInterface.php b/novaconium/vendor/twig/src/Node/Expression/ReturnArrayInterface.php new file mode 100644 index 0000000..a74864b --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/ReturnArrayInterface.php @@ -0,0 +1,16 @@ + + */ +trait SupportDefinedTestDeprecationTrait +{ + public function getAttribute($name, $default = null) + { + if ('is_defined_test' === $name) { + trigger_deprecation('twig/twig', '3.21', 'The "is_defined_test" attribute is deprecated, call "isDefinedTestEnabled()" instead.'); + + return $this->isDefinedTestEnabled(); + } + + return parent::getAttribute($name, $default); + } + + public function setAttribute(string $name, $value): void + { + if ('is_defined_test' === $name) { + trigger_deprecation('twig/twig', '3.21', 'The "is_defined_test" attribute is deprecated, call "enableDefinedTest()" instead.'); + + $this->definedTest = (bool) $value; + } else { + parent::setAttribute($name, $value); + } + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/SupportDefinedTestInterface.php b/novaconium/vendor/twig/src/Node/Expression/SupportDefinedTestInterface.php new file mode 100644 index 0000000..450c691 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/SupportDefinedTestInterface.php @@ -0,0 +1,24 @@ + + */ +interface SupportDefinedTestInterface +{ + public function enableDefinedTest(): void; + + public function isDefinedTestEnabled(): bool; +} diff --git a/novaconium/vendor/twig/src/Node/Expression/SupportDefinedTestTrait.php b/novaconium/vendor/twig/src/Node/Expression/SupportDefinedTestTrait.php new file mode 100644 index 0000000..4cf1a58 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/SupportDefinedTestTrait.php @@ -0,0 +1,27 @@ +definedTest = true; + } + + public function isDefinedTestEnabled(): bool + { + return $this->definedTest; + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/TempNameExpression.php b/novaconium/vendor/twig/src/Node/Expression/TempNameExpression.php new file mode 100644 index 0000000..f996aab --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/TempNameExpression.php @@ -0,0 +1,49 @@ + $name], $lineno); + } + + public function compile(Compiler $compiler): void + { + if (null === $this->getAttribute('name')) { + $this->setAttribute('name', $compiler->getVarName()); + } + + $compiler->raw('$'.$this->getAttribute('name')); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Ternary/ConditionalTernary.php b/novaconium/vendor/twig/src/Node/Expression/Ternary/ConditionalTernary.php new file mode 100644 index 0000000..3972755 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Ternary/ConditionalTernary.php @@ -0,0 +1,43 @@ + TrueTest::wrap($test), 'left' => $left, 'right' => $right], [], $lineno); + } + + public function compile(Compiler $compiler): void + { + $compiler + ->raw('((') + ->subcompile($this->getNode('test')) + ->raw(') ? (') + ->subcompile($this->getNode('left')) + ->raw(') : (') + ->subcompile($this->getNode('right')) + ->raw('))') + ; + } + + public function getOperandNamesToEscape(): array + { + return ['left', 'right']; + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Test/ConstantTest.php b/novaconium/vendor/twig/src/Node/Expression/Test/ConstantTest.php new file mode 100644 index 0000000..867fd09 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Test/ConstantTest.php @@ -0,0 +1,49 @@ + + */ +class ConstantTest extends TestExpression +{ + public function compile(Compiler $compiler): void + { + $compiler + ->raw('(') + ->subcompile($this->getNode('node')) + ->raw(' === constant(') + ; + + if ($this->getNode('arguments')->hasNode('1')) { + $compiler + ->raw('get_class(') + ->subcompile($this->getNode('arguments')->getNode('1')) + ->raw(')."::".') + ; + } + + $compiler + ->subcompile($this->getNode('arguments')->getNode('0')) + ->raw('))') + ; + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Test/DefinedTest.php b/novaconium/vendor/twig/src/Node/Expression/Test/DefinedTest.php new file mode 100644 index 0000000..73e4372 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Test/DefinedTest.php @@ -0,0 +1,68 @@ + + */ +class DefinedTest extends TestExpression +{ + /** + * @param AbstractExpression $node + */ + #[FirstClassTwigCallableReady] + public function __construct(Node $node, TwigTest|string $name, ?Node $arguments, int $lineno) + { + if (!$node instanceof AbstractExpression) { + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance to the "node" argument of "%s" is deprecated ("%s" given).', AbstractExpression::class, static::class, $node::class); + } + + if (!$node instanceof SupportDefinedTestInterface) { + throw new SyntaxError('The "defined" test only works with simple variables.', $lineno); + } + + $node->enableDefinedTest(); + + if (\is_string($name) && 'defined' !== $name) { + trigger_deprecation('twig/twig', '3.12', 'Creating a "DefinedTest" instance with a test name that is not "defined" is deprecated.'); + } + + parent::__construct($node, $name, $arguments, $lineno); + } + + public function compile(Compiler $compiler): void + { + $compiler->subcompile($this->getNode('node')); + } + + public function getStringCoercedChildNames(): array + { + // the `defined` test does not coerce its node to string (it only inspects existence) + return []; + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Test/DivisiblebyTest.php b/novaconium/vendor/twig/src/Node/Expression/Test/DivisiblebyTest.php new file mode 100644 index 0000000..ab9dd41 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Test/DivisiblebyTest.php @@ -0,0 +1,42 @@ + + */ +class DivisiblebyTest extends TestExpression +{ + public function compile(Compiler $compiler): void + { + $compiler + ->raw('(0 == ') + ->subcompile($this->getNode('node')) + ->raw(' % ') + ->subcompile($this->getNode('arguments')->getNode('0')) + ->raw(')') + ; + } + + public function getStringCoercedChildNames(): array + { + // PHP `%` rejects Stringable with a TypeError, no coercion + return []; + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Test/EvenTest.php b/novaconium/vendor/twig/src/Node/Expression/Test/EvenTest.php new file mode 100644 index 0000000..81a06aa --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Test/EvenTest.php @@ -0,0 +1,41 @@ + + */ +class EvenTest extends TestExpression +{ + public function compile(Compiler $compiler): void + { + $compiler + ->raw('(') + ->subcompile($this->getNode('node')) + ->raw(' % 2 == 0') + ->raw(')') + ; + } + + public function getStringCoercedChildNames(): array + { + // PHP `%` rejects Stringable with a TypeError, no coercion + return []; + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Test/NullTest.php b/novaconium/vendor/twig/src/Node/Expression/Test/NullTest.php new file mode 100644 index 0000000..904aef9 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Test/NullTest.php @@ -0,0 +1,40 @@ + + */ +class NullTest extends TestExpression +{ + public function compile(Compiler $compiler): void + { + $compiler + ->raw('(null === ') + ->subcompile($this->getNode('node')) + ->raw(')') + ; + } + + public function getStringCoercedChildNames(): array + { + // `=== null` is strict, no coercion + return []; + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Test/OddTest.php b/novaconium/vendor/twig/src/Node/Expression/Test/OddTest.php new file mode 100644 index 0000000..967c353 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Test/OddTest.php @@ -0,0 +1,41 @@ + + */ +class OddTest extends TestExpression +{ + public function compile(Compiler $compiler): void + { + $compiler + ->raw('(') + ->subcompile($this->getNode('node')) + ->raw(' % 2 != 0') + ->raw(')') + ; + } + + public function getStringCoercedChildNames(): array + { + // PHP `%` rejects Stringable with a TypeError, no coercion + return []; + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Test/SameasTest.php b/novaconium/vendor/twig/src/Node/Expression/Test/SameasTest.php new file mode 100644 index 0000000..cc4d723 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Test/SameasTest.php @@ -0,0 +1,40 @@ + + */ +class SameasTest extends TestExpression +{ + public function compile(Compiler $compiler): void + { + $compiler + ->raw('(') + ->subcompile($this->getNode('node')) + ->raw(' === ') + ->subcompile($this->getNode('arguments')->getNode('0')) + ->raw(')') + ; + } + + public function getStringCoercedChildNames(): array + { + // `===` is strict, no coercion + return []; + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Test/TrueTest.php b/novaconium/vendor/twig/src/Node/Expression/Test/TrueTest.php new file mode 100644 index 0000000..f0c49d4 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Test/TrueTest.php @@ -0,0 +1,52 @@ + + */ +class TrueTest extends TestExpression +{ + public static function wrap(Node $node): Node + { + if ($node instanceof ReturnPrimitiveTypeInterface) { + return $node; + } + + return new self($node, new TwigTest('true', null, ['always_allowed_in_sandbox' => true]), null, $node->getTemplateLine()); + } + + public function compile(Compiler $compiler): void + { + $compiler + ->raw('(($tmp = ') + ->subcompile($this->getNode('node')) + ->raw(') && $tmp instanceof Markup ? (string) $tmp : $tmp)') + ; + } + + public function getStringCoercedChildNames(): array + { + // the `(string)` cast only fires for Markup instances, whose __toString is always allowed + return []; + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/TestExpression.php b/novaconium/vendor/twig/src/Node/Expression/TestExpression.php new file mode 100644 index 0000000..4d81239 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/TestExpression.php @@ -0,0 +1,91 @@ + $node]; + if (null !== $arguments) { + $nodes['arguments'] = $arguments; + } + + if ($test instanceof TwigTest) { + $name = $test->getName(); + } else { + $name = $test; + trigger_deprecation('twig/twig', '3.12', 'Not passing an instance of "TwigTest" when creating a "%s" test of type "%s" is deprecated.', $name, static::class); + } + + parent::__construct($nodes, ['name' => $name, 'type' => 'test'], $lineno); + + if ($test instanceof TwigTest) { + $this->setAttribute('twig_callable', $test); + } + + $this->deprecateAttribute('arguments', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('callable', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('is_variadic', new NameDeprecation('twig/twig', '3.12')); + $this->deprecateAttribute('dynamic_name', new NameDeprecation('twig/twig', '3.12')); + } + + public function compile(Compiler $compiler): void + { + $name = $this->getAttribute('name'); + if ($this->hasAttribute('twig_callable')) { + $name = $this->getAttribute('twig_callable')->getName(); + if ($name !== $this->getAttribute('name')) { + trigger_deprecation('twig/twig', '3.12', 'Changing the value of a "test" node in a NodeVisitor class is not supported anymore.'); + $this->removeAttribute('twig_callable'); + } + } + + if (!$this->hasAttribute('twig_callable')) { + $this->setAttribute('twig_callable', $compiler->getEnvironment()->getTest($this->getAttribute('name'))); + } + + $this->compileCallable($compiler); + } + + public function getStringCoercedChildNames(): array + { + $names = []; + + // the `empty` test triggers an implicit string coercion through `CoreExtension::testEmpty()` + if ('empty' === $this->getAttribute('name')) { + $names[] = 'node'; + } + + // a test may coerce its arguments to string (the host PHP code is opaque to Twig) + if ($this->hasNode('arguments')) { + $names[] = 'arguments'; + } + + return $names; + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Unary/AbstractUnary.php b/novaconium/vendor/twig/src/Node/Expression/Unary/AbstractUnary.php new file mode 100644 index 0000000..09f3d09 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Unary/AbstractUnary.php @@ -0,0 +1,48 @@ + $node], ['with_parentheses' => false], $lineno); + } + + public function compile(Compiler $compiler): void + { + if ($this->hasExplicitParentheses()) { + $compiler->raw('('); + } else { + $compiler->raw(' '); + } + $this->operator($compiler); + $compiler->subcompile($this->getNode('node')); + if ($this->hasExplicitParentheses()) { + $compiler->raw(')'); + } + } + + abstract public function operator(Compiler $compiler): Compiler; +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Unary/NegUnary.php b/novaconium/vendor/twig/src/Node/Expression/Unary/NegUnary.php new file mode 100644 index 0000000..dc2f235 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Unary/NegUnary.php @@ -0,0 +1,23 @@ +raw('-'); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Unary/NotUnary.php b/novaconium/vendor/twig/src/Node/Expression/Unary/NotUnary.php new file mode 100644 index 0000000..6bdf880 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Unary/NotUnary.php @@ -0,0 +1,31 @@ +raw('!'); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Unary/PosUnary.php b/novaconium/vendor/twig/src/Node/Expression/Unary/PosUnary.php new file mode 100644 index 0000000..4b0a062 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Unary/PosUnary.php @@ -0,0 +1,23 @@ +raw('+'); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Unary/SpreadUnary.php b/novaconium/vendor/twig/src/Node/Expression/Unary/SpreadUnary.php new file mode 100644 index 0000000..f99072c --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Unary/SpreadUnary.php @@ -0,0 +1,22 @@ +raw('...'); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Unary/StringCastUnary.php b/novaconium/vendor/twig/src/Node/Expression/Unary/StringCastUnary.php new file mode 100644 index 0000000..87ea17c --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Unary/StringCastUnary.php @@ -0,0 +1,22 @@ +raw('(string)'); + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Unary/UnaryInterface.php b/novaconium/vendor/twig/src/Node/Expression/Unary/UnaryInterface.php new file mode 100644 index 0000000..b094ef4 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Unary/UnaryInterface.php @@ -0,0 +1,22 @@ + $var], ['global' => $global], $var->getTemplateLine()); + } + + public function compile(Compiler $compiler): void + { + /** @var TemplateVariable $var */ + $var = $this->nodes['var']; + + $compiler + ->addDebugInfo($this) + ->write('$macros[') + ->string($var->getName($compiler)) + ->raw('] = ') + ; + + if ($this->getAttribute('global')) { + $compiler + ->raw('$this->macros[') + ->string($var->getName($compiler)) + ->raw('] = ') + ; + } + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/Variable/ContextVariable.php b/novaconium/vendor/twig/src/Node/Expression/Variable/ContextVariable.php new file mode 100644 index 0000000..01bbcb7 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/Variable/ContextVariable.php @@ -0,0 +1,18 @@ +getAttribute('name')) { + $this->setAttribute('name', $compiler->getVarName()); + } + + return $this->getAttribute('name'); + } + + public function compile(Compiler $compiler): void + { + $name = $this->getName($compiler); + + if ('_self' === $name) { + $compiler->raw('$this'); + } else { + $compiler + ->raw('$macros[') + ->string($name) + ->raw(']') + ; + } + } +} diff --git a/novaconium/vendor/twig/src/Node/Expression/VariadicExpression.php b/novaconium/vendor/twig/src/Node/Expression/VariadicExpression.php new file mode 100644 index 0000000..a1bdb48 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Expression/VariadicExpression.php @@ -0,0 +1,24 @@ +raw('...'); + + parent::compile($compiler); + } +} diff --git a/novaconium/vendor/twig/src/Node/FlushNode.php b/novaconium/vendor/twig/src/Node/FlushNode.php new file mode 100644 index 0000000..ff3bd1c --- /dev/null +++ b/novaconium/vendor/twig/src/Node/FlushNode.php @@ -0,0 +1,40 @@ + + */ +#[YieldReady] +class FlushNode extends Node +{ + public function __construct(int $lineno) + { + parent::__construct([], [], $lineno); + } + + public function compile(Compiler $compiler): void + { + $compiler->addDebugInfo($this); + + if ($compiler->getEnvironment()->useYield()) { + $compiler->write("yield '';\n"); + } + + $compiler->write("flush();\n"); + } +} diff --git a/novaconium/vendor/twig/src/Node/ForElseNode.php b/novaconium/vendor/twig/src/Node/ForElseNode.php new file mode 100644 index 0000000..56d6646 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/ForElseNode.php @@ -0,0 +1,41 @@ + + */ +#[YieldReady] +class ForElseNode extends Node +{ + public function __construct(Node $body, int $lineno) + { + parent::__construct(['body' => $body], [], $lineno); + } + + public function compile(Compiler $compiler): void + { + $compiler + ->addDebugInfo($this) + ->write("if (!\$context['_iterated']) {\n") + ->indent() + ->subcompile($this->getNode('body')) + ->outdent() + ->write("}\n") + ; + } +} diff --git a/novaconium/vendor/twig/src/Node/ForLoopNode.php b/novaconium/vendor/twig/src/Node/ForLoopNode.php new file mode 100644 index 0000000..1f0a4f3 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/ForLoopNode.php @@ -0,0 +1,51 @@ + + */ +#[YieldReady] +class ForLoopNode extends Node +{ + public function __construct(int $lineno) + { + parent::__construct([], ['with_loop' => false, 'ifexpr' => false, 'else' => false], $lineno); + } + + public function compile(Compiler $compiler): void + { + if ($this->getAttribute('else')) { + $compiler->write("\$context['_iterated'] = true;\n"); + } + + if ($this->getAttribute('with_loop')) { + $compiler + ->write("++\$context['loop']['index0'];\n") + ->write("++\$context['loop']['index'];\n") + ->write("\$context['loop']['first'] = false;\n") + ->write("if (isset(\$context['loop']['revindex0'], \$context['loop']['revindex'])) {\n") + ->indent() + ->write("--\$context['loop']['revindex0'];\n") + ->write("--\$context['loop']['revindex'];\n") + ->write("\$context['loop']['last'] = 0 === \$context['loop']['revindex0'];\n") + ->outdent() + ->write("}\n") + ; + } + } +} diff --git a/novaconium/vendor/twig/src/Node/ForNode.php b/novaconium/vendor/twig/src/Node/ForNode.php new file mode 100644 index 0000000..fda4071 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/ForNode.php @@ -0,0 +1,123 @@ + + */ +#[YieldReady] +class ForNode extends Node +{ + private $loop; + + public function __construct(AssignContextVariable $keyTarget, AssignContextVariable $valueTarget, AbstractExpression $seq, ?Node $ifexpr, Node $body, ?Node $else, int $lineno) + { + $body = new Nodes([$body, $this->loop = new ForLoopNode($lineno)]); + + if (null !== $ifexpr) { + trigger_deprecation('twig/twig', '3.19', \sprintf('Passing not-null to the "ifexpr" argument of the "%s" constructor is deprecated.', static::class)); + } + + if (null !== $else && !$else instanceof ForElseNode) { + trigger_deprecation('twig/twig', '3.19', \sprintf('Not passing an instance of "%s" to the "else" argument of the "%s" constructor is deprecated.', ForElseNode::class, static::class)); + + $else = new ForElseNode($else, $else->getTemplateLine()); + } + + $nodes = ['key_target' => $keyTarget, 'value_target' => $valueTarget, 'seq' => $seq, 'body' => $body]; + if (null !== $else) { + $nodes['else'] = $else; + } + + parent::__construct($nodes, ['with_loop' => true], $lineno); + } + + public function compile(Compiler $compiler): void + { + $compiler + ->addDebugInfo($this) + ->write("\$context['_parent'] = \$context;\n") + ->write("\$context['_seq'] = CoreExtension::ensureTraversable(") + ->subcompile($this->getNode('seq')) + ->raw(");\n") + ; + + if ($this->hasNode('else')) { + $compiler->write("\$context['_iterated'] = false;\n"); + } + + if ($this->getAttribute('with_loop')) { + $compiler + ->write("\$context['loop'] = [\n") + ->write(" 'parent' => \$context['_parent'],\n") + ->write(" 'index0' => 0,\n") + ->write(" 'index' => 1,\n") + ->write(" 'first' => true,\n") + ->write("];\n") + ->write("if (is_array(\$context['_seq']) || (is_object(\$context['_seq']) && \$context['_seq'] instanceof \Countable)) {\n") + ->indent() + ->write("\$length = count(\$context['_seq']);\n") + ->write("\$context['loop']['revindex0'] = \$length - 1;\n") + ->write("\$context['loop']['revindex'] = \$length;\n") + ->write("\$context['loop']['length'] = \$length;\n") + ->write("\$context['loop']['last'] = 1 === \$length;\n") + ->outdent() + ->write("}\n") + ; + } + + $this->loop->setAttribute('else', $this->hasNode('else')); + $this->loop->setAttribute('with_loop', $this->getAttribute('with_loop')); + + $compiler + ->write("foreach (\$context['_seq'] as ") + ->subcompile($this->getNode('key_target')) + ->raw(' => ') + ->subcompile($this->getNode('value_target')) + ->raw(") {\n") + ->indent() + ->subcompile($this->getNode('body')) + ->outdent() + ->write("}\n") + ; + + if ($this->hasNode('else')) { + $compiler->subcompile($this->getNode('else')); + } + + $compiler->write("\$_parent = \$context['_parent'];\n"); + + // remove some "private" loop variables (needed for nested loops) + $compiler->write('unset($context[\'_seq\'], $context[\''.$this->getNode('key_target')->getAttribute('name').'\'], $context[\''.$this->getNode('value_target')->getAttribute('name').'\'], $context[\'_parent\']'); + if ($this->hasNode('else')) { + $compiler->raw(', $context[\'_iterated\']'); + } + if ($this->getAttribute('with_loop')) { + $compiler->raw(', $context[\'loop\']'); + } + $compiler->raw(");\n"); + + // keep the values set in the inner context for variables defined in the outer context + $compiler + ->write("\$context = array_intersect_key(\$context, \$_parent);\n") + ->write("\$context += \$_parent;\n") + ; + } +} diff --git a/novaconium/vendor/twig/src/Node/IfNode.php b/novaconium/vendor/twig/src/Node/IfNode.php new file mode 100644 index 0000000..c7a7f4b --- /dev/null +++ b/novaconium/vendor/twig/src/Node/IfNode.php @@ -0,0 +1,79 @@ + + */ +#[YieldReady] +class IfNode extends Node +{ + public function __construct(Node $tests, ?Node $else, int $lineno) + { + for ($i = 0, $count = \count($tests); $i < $count; $i += 2) { + $tests->setNode($i, TrueTest::wrap($tests->getNode((string) $i))); + } + $nodes = ['tests' => $tests]; + if (null !== $else) { + $nodes['else'] = $else; + } + + parent::__construct($nodes, [], $lineno); + } + + public function compile(Compiler $compiler): void + { + $compiler->addDebugInfo($this); + for ($i = 0, $count = \count($this->getNode('tests')); $i < $count; $i += 2) { + if ($i > 0) { + $compiler + ->outdent() + ->write('} elseif (') + ; + } else { + $compiler + ->write('if (') + ; + } + + $compiler + ->subcompile($this->getNode('tests')->getNode((string) $i)) + ->raw(") {\n") + ->indent() + ; + // The node might not exists if the content is empty + if ($this->getNode('tests')->hasNode((string) ($i + 1))) { + $compiler->subcompile($this->getNode('tests')->getNode((string) ($i + 1))); + } + } + + if ($this->hasNode('else')) { + $compiler + ->outdent() + ->write("} else {\n") + ->indent() + ->subcompile($this->getNode('else')) + ; + } + + $compiler + ->outdent() + ->write("}\n"); + } +} diff --git a/novaconium/vendor/twig/src/Node/ImportNode.php b/novaconium/vendor/twig/src/Node/ImportNode.php new file mode 100644 index 0000000..ab7fe0f --- /dev/null +++ b/novaconium/vendor/twig/src/Node/ImportNode.php @@ -0,0 +1,67 @@ + + */ +#[YieldReady] +class ImportNode extends Node implements CoercesChildrenToStringInterface +{ + public function __construct(AbstractExpression $expr, AbstractExpression|AssignTemplateVariable $var, int $lineno) + { + if (\func_num_args() > 3) { + trigger_deprecation('twig/twig', '3.15', \sprintf('Passing more than 3 arguments to "%s()" is deprecated.', __METHOD__)); + } + + if (!$var instanceof AssignTemplateVariable) { + trigger_deprecation('twig/twig', '3.15', \sprintf('Passing a "%s" instance as the second argument of "%s" is deprecated, pass a "%s" instead.', $var::class, __CLASS__, AssignTemplateVariable::class)); + + $var = new AssignTemplateVariable($var->getAttribute('name'), $lineno); + } + + parent::__construct(['expr' => $expr, 'var' => $var], [], $lineno); + } + + public function compile(Compiler $compiler): void + { + $compiler->subcompile($this->getNode('var')); + + if ($this->getNode('expr') instanceof ContextVariable && '_self' === $this->getNode('expr')->getAttribute('name')) { + $compiler->raw('$this'); + } else { + $compiler + ->raw('$this->load(') + ->subcompile($this->getNode('expr')) + ->raw(', ') + ->repr($this->getTemplateLine()) + ->raw(')->unwrap()') + ; + } + + $compiler->raw(";\n"); + } + + public function getStringCoercedChildNames(): array + { + // the loader resolves the template-name expression by coercing it to a string + return ['expr']; + } +} diff --git a/novaconium/vendor/twig/src/Node/IncludeNode.php b/novaconium/vendor/twig/src/Node/IncludeNode.php new file mode 100644 index 0000000..3adf415 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/IncludeNode.php @@ -0,0 +1,121 @@ + + */ +#[YieldReady] +class IncludeNode extends Node implements NodeOutputInterface, CoercesChildrenToStringInterface +{ + public function __construct(AbstractExpression $expr, ?AbstractExpression $variables, bool $only, bool $ignoreMissing, int $lineno) + { + $nodes = ['expr' => $expr]; + if (null !== $variables) { + $nodes['variables'] = $variables; + } + + parent::__construct($nodes, ['only' => $only, 'ignore_missing' => $ignoreMissing], $lineno); + } + + public function compile(Compiler $compiler): void + { + $compiler->addDebugInfo($this); + + if ($this->getAttribute('ignore_missing')) { + $template = $compiler->getVarName(); + + $compiler + ->write("try {\n") + ->indent() + ->write(\sprintf('$%s = ', $template)) + ; + + $this->addGetTemplate($compiler, $template); + + $compiler + ->raw(";\n") + ->outdent() + ->write("} catch (LoaderError \$e) {\n") + ->indent() + ->write("// ignore missing template\n") + ->write(\sprintf("\$$template = null;\n", $template)) + ->outdent() + ->write("}\n") + ->write(\sprintf("if ($%s) {\n", $template)) + ->indent() + ; + + $compiler->write(\sprintf('yield from $%s->unwrap()->yield(', $template)); + + $this->addTemplateArguments($compiler); + $compiler + ->raw(");\n") + ->outdent() + ->write("}\n") + ; + } else { + $compiler->write('yield from '); + $this->addGetTemplate($compiler); + $compiler->raw('->unwrap()->yield('); + $this->addTemplateArguments($compiler); + $compiler->raw(");\n"); + } + } + + /** + * @return void + */ + protected function addGetTemplate(Compiler $compiler/* , string $template = '' */) + { + $compiler + ->raw('$this->load(') + ->subcompile($this->getNode('expr')) + ->raw(', ') + ->repr($this->getTemplateLine()) + ->raw(')') + ; + } + + /** + * @return void + */ + protected function addTemplateArguments(Compiler $compiler) + { + if (!$this->hasNode('variables')) { + $compiler->raw(false === $this->getAttribute('only') ? '$context' : '[]'); + } elseif (false === $this->getAttribute('only')) { + $compiler + ->raw('CoreExtension::merge($context, ') + ->subcompile($this->getNode('variables')) + ->raw(')') + ; + } else { + $compiler->raw('CoreExtension::toArray('); + $compiler->subcompile($this->getNode('variables')); + $compiler->raw(')'); + } + } + + public function getStringCoercedChildNames(): array + { + // the loader resolves the template-name expression by coercing it to a string + return ['expr']; + } +} diff --git a/novaconium/vendor/twig/src/Node/MacroNode.php b/novaconium/vendor/twig/src/Node/MacroNode.php new file mode 100644 index 0000000..db3ca45 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/MacroNode.php @@ -0,0 +1,121 @@ + + */ +#[YieldReady] +class MacroNode extends Node +{ + public const VARARGS_NAME = 'varargs'; + + /** + * @param BodyNode $body + * @param ArrayExpression $arguments + */ + public function __construct(string $name, Node $body, Node $arguments, int $lineno) + { + if (!$body instanceof BodyNode) { + trigger_deprecation('twig/twig', '3.12', \sprintf('Not passing a "%s" instance as the "body" argument of the "%s" constructor is deprecated ("%s" given).', BodyNode::class, static::class, $body::class)); + } + + if (!$arguments instanceof ArrayExpression) { + trigger_deprecation('twig/twig', '3.15', \sprintf('Not passing a "%s" instance as the "arguments" argument of the "%s" constructor is deprecated ("%s" given).', ArrayExpression::class, static::class, $arguments::class)); + + $args = new ArrayExpression([], $arguments->getTemplateLine()); + foreach ($arguments as $n => $default) { + $args->addElement($default, new LocalVariable($n, $default->getTemplateLine())); + } + $arguments = $args; + } + + foreach ($arguments->getKeyValuePairs() as $pair) { + if ("\u{035C}".self::VARARGS_NAME === $pair['key']->getAttribute('name')) { + throw new SyntaxError(\sprintf('The argument "%s" in macro "%s" cannot be defined because the variable "%s" is reserved for arbitrary arguments.', self::VARARGS_NAME, $name, self::VARARGS_NAME), $pair['value']->getTemplateLine(), $pair['value']->getSourceContext()); + } + } + + parent::__construct(['body' => $body, 'arguments' => $arguments], ['name' => $name], $lineno); + } + + public function compile(Compiler $compiler): void + { + $compiler + ->addDebugInfo($this) + ->write(\sprintf('public function macro_%s(', $this->getAttribute('name'))) + ; + + /** @var ArrayExpression $arguments */ + $arguments = $this->getNode('arguments'); + foreach ($arguments->getKeyValuePairs() as $pair) { + $name = $pair['key']; + $default = $pair['value']; + $compiler + ->subcompile($name) + ->raw(' = ') + ->subcompile($default) + ->raw(', ') + ; + } + + $compiler + ->raw('...$varargs') + ->raw("): string|Markup\n") + ->write("{\n") + ->indent() + ->write("\$macros = \$this->macros;\n") + ->write("\$context = [\n") + ->indent() + ; + + foreach ($arguments->getKeyValuePairs() as $pair) { + $name = $pair['key']; + $var = $name->getAttribute('name'); + if (str_starts_with($var, "\u{035C}")) { + $var = substr($var, \strlen("\u{035C}")); + } + $compiler + ->write('') + ->string($var) + ->raw(' => ') + ->subcompile($name) + ->raw(",\n") + ; + } + + $node = new CaptureNode($this->getNode('body'), $this->getNode('body')->lineno); + + $compiler + ->write('') + ->string(self::VARARGS_NAME) + ->raw(' => ') + ->raw("\$varargs,\n") + ->outdent() + ->write("] + \$this->env->getGlobals();\n\n") + ->write("\$blocks = [];\n\n") + ->write('return ') + ->subcompile($node) + ->raw("\n") + ->outdent() + ->write("}\n\n") + ; + } +} diff --git a/novaconium/vendor/twig/src/Node/ModuleNode.php b/novaconium/vendor/twig/src/Node/ModuleNode.php new file mode 100644 index 0000000..fd43246 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/ModuleNode.php @@ -0,0 +1,522 @@ + + */ +#[YieldReady] +final class ModuleNode extends Node implements CoercesChildrenToStringInterface +{ + /** + * @param BodyNode $body + */ + public function __construct(Node $body, ?AbstractExpression $parent, Node $blocks, Node $macros, Node $traits, $embeddedTemplates, Source $source) + { + if (!$body instanceof BodyNode) { + trigger_deprecation('twig/twig', '3.12', \sprintf('Not passing a "%s" instance as the "body" argument of the "%s" constructor is deprecated.', BodyNode::class, static::class)); + } + if (!$embeddedTemplates instanceof Node) { + trigger_deprecation('twig/twig', '3.21', \sprintf('Not passing a "%s" instance as the "embedded_templates" argument of the "%s" constructor is deprecated.', Node::class, static::class)); + + if (null !== $embeddedTemplates) { + $embeddedTemplates = new Nodes($embeddedTemplates); + } else { + $embeddedTemplates = new EmptyNode(); + } + } + + $nodes = [ + 'body' => $body, + 'blocks' => $blocks, + 'macros' => $macros, + 'traits' => $traits, + 'display_start' => new Nodes(), + 'display_end' => new Nodes(), + 'constructor_start' => new Nodes(), + 'constructor_end' => new Nodes(), + 'class_end' => new Nodes(), + ]; + if (null !== $parent) { + $nodes['parent'] = $parent; + } + + // embedded templates are set as attributes so that they are only visited once by the visitors + parent::__construct($nodes, [ + 'index' => null, + 'embedded_templates' => $embeddedTemplates, + ], 1); + + // populate the template name of all node children + $this->setSourceContext($source); + } + + /** + * @return void + */ + public function setIndex($index) + { + $this->setAttribute('index', $index); + } + + public function compile(Compiler $compiler): void + { + $this->compileTemplate($compiler); + + foreach ($this->getAttribute('embedded_templates') as $template) { + $compiler->subcompile($template); + } + } + + public function getStringCoercedChildNames(): array + { + // the parent expression is resolved through the loader, which coerces it to a string + return $this->hasNode('parent') ? ['parent'] : []; + } + + /** + * @return void + */ + protected function compileTemplate(Compiler $compiler) + { + if (!$this->getAttribute('index')) { + $compiler->write('compileClassHeader($compiler); + + $this->compileConstructor($compiler); + + $this->compileGetParent($compiler); + + $this->compileDisplay($compiler); + + $compiler->subcompile($this->getNode('blocks')); + + $this->compileMacros($compiler); + + $this->compileGetTemplateName($compiler); + + $this->compileIsTraitable($compiler); + + $this->compileDebugInfo($compiler); + + $this->compileGetSourceContext($compiler); + + $this->compileClassFooter($compiler); + } + + /** + * @return void + */ + protected function compileGetParent(Compiler $compiler) + { + if (!$this->hasNode('parent')) { + return; + } + $parent = $this->getNode('parent'); + + $compiler + ->write("protected function doGetParent(array \$context): bool|string|Template|TemplateWrapper\n", "{\n") + ->indent() + ->addDebugInfo($parent) + ->write('return ') + ; + + if ($parent instanceof ConstantExpression) { + $compiler->subcompile($parent); + } else { + $compiler + ->raw('$this->load(') + ->subcompile($parent) + ->raw(', ') + ->repr($parent->getTemplateLine()) + ->raw(')') + ; + } + + $compiler + ->raw(";\n") + ->outdent() + ->write("}\n\n") + ; + } + + /** + * @return void + */ + protected function compileClassHeader(Compiler $compiler) + { + $compiler + ->write("\n\n") + ; + if (!$this->getAttribute('index')) { + $compiler + ->write("use Twig\Environment;\n") + ->write("use Twig\Error\LoaderError;\n") + ->write("use Twig\Error\RuntimeError;\n") + ->write("use Twig\Extension\CoreExtension;\n") + ->write("use Twig\Extension\SandboxExtension;\n") + ->write("use Twig\Markup;\n") + ->write("use Twig\Sandbox\SecurityError;\n") + ->write("use Twig\Sandbox\SecurityNotAllowedTagError;\n") + ->write("use Twig\Sandbox\SecurityNotAllowedFilterError;\n") + ->write("use Twig\Sandbox\SecurityNotAllowedFunctionError;\n") + ->write("use Twig\Sandbox\SecurityNotAllowedTestError;\n") + ->write("use Twig\Source;\n") + ->write("use Twig\Template;\n") + ->write("use Twig\TemplateWrapper;\n") + ->write("\n") + ; + } + $compiler + // if the template name contains */, add a blank to avoid a PHP parse error + ->write('/* '.str_replace('*/', '* /', $this->getSourceContext()->getName())." */\n") + ->write('class '.$compiler->getEnvironment()->getTemplateClass($this->getSourceContext()->getName(), $this->getAttribute('index'))) + ->raw(" extends Template\n") + ->write("{\n") + ->indent() + ->write("private Source \$source;\n") + ->write("/**\n") + ->write(" * @var array\n") + ->write(" */\n") + ->write("private array \$macros = [];\n\n") + ; + } + + /** + * @return void + */ + protected function compileConstructor(Compiler $compiler) + { + $compiler + ->write("public function __construct(Environment \$env)\n", "{\n") + ->indent() + ->subcompile($this->getNode('constructor_start')) + ->write("parent::__construct(\$env);\n\n") + ->write("\$this->source = \$this->getSourceContext();\n\n") + ; + + // parent + if (!$this->hasNode('parent')) { + $compiler->write("\$this->parent = false;\n\n"); + } + + $countTraits = \count($this->getNode('traits')); + if ($countTraits) { + // traits + foreach ($this->getNode('traits') as $i => $trait) { + $node = $trait->getNode('template'); + + $compiler + ->addDebugInfo($node) + ->write(\sprintf('$_trait_%s = $this->load(', $i)) + ->subcompile($node) + ->raw(', ') + ->repr($node->getTemplateLine()) + ->raw(");\n") + ->write(\sprintf("if (!\$_trait_%s->unwrap()->isTraitable()) {\n", $i)) + ->indent() + ->write("throw new RuntimeError('Template \"'.") + ->subcompile($trait->getNode('template')) + ->raw(".'\" cannot be used as a trait.', ") + ->repr($node->getTemplateLine()) + ->raw(", \$this->source);\n") + ->outdent() + ->write("}\n") + ->write(\sprintf("\$_trait_%s_blocks = \$_trait_%s->unwrap()->getBlocks();\n\n", $i, $i)) + ; + + foreach ($trait->getNode('targets') as $key => $value) { + $compiler + ->write(\sprintf('if (!isset($_trait_%s_blocks[', $i)) + ->string($key) + ->raw("])) {\n") + ->indent() + ->write("throw new RuntimeError(sprintf('Block \"%s\" is not defined in trait \"%s\".', ") + ->string($key) + ->raw(', ') + ->subcompile($trait->getNode('template')) + ->raw('), ') + ->repr($node->getTemplateLine()) + ->raw(", \$this->source);\n") + ->outdent() + ->write("}\n\n") + + ->write(\sprintf('$_trait_%s_blocks[', $i)) + ->subcompile($value) + ->raw(\sprintf('] = $_trait_%s_blocks[', $i)) + ->string($key) + ->raw(\sprintf(']; unset($_trait_%s_blocks[', $i)) + ->string($key) + ->raw(']); $this->traitAliases[') + ->subcompile($value) + ->raw('] = ') + ->string($key) + ->raw(";\n\n") + ; + } + } + + if ($countTraits > 1) { + $compiler + ->write("\$this->traits = array_merge(\n") + ->indent() + ; + + for ($i = 0; $i < $countTraits; ++$i) { + $compiler + ->write(\sprintf('$_trait_%s_blocks'.($i == $countTraits - 1 ? '' : ',')."\n", $i)) + ; + } + + $compiler + ->outdent() + ->write(");\n\n") + ; + } else { + $compiler + ->write("\$this->traits = \$_trait_0_blocks;\n\n") + ; + } + + $compiler + ->write("\$this->blocks = array_merge(\n") + ->indent() + ->write("\$this->traits,\n") + ->write("[\n") + ; + } else { + $compiler + ->write("\$this->blocks = [\n") + ; + } + + // blocks + $compiler + ->indent() + ; + + foreach ($this->getNode('blocks') as $name => $node) { + $compiler + ->write(\sprintf("'%s' => [\$this, 'block_%s'],\n", $name, $name)) + ; + } + + if ($countTraits) { + $compiler + ->outdent() + ->write("]\n") + ->outdent() + ->write(");\n") + ; + } else { + $compiler + ->outdent() + ->write("];\n") + ; + } + + $compiler + ->subcompile($this->getNode('constructor_end')) + ->outdent() + ->write("}\n\n") + ; + } + + /** + * @return void + */ + protected function compileDisplay(Compiler $compiler) + { + $compiler + ->write("protected function doDisplay(array \$context, array \$blocks = []): iterable\n", "{\n") + ->indent() + ->write("\$macros = \$this->macros;\n") + ->subcompile($this->getNode('display_start')) + ->subcompile($this->getNode('body')) + ; + + if ($this->hasNode('parent')) { + $parent = $this->getNode('parent'); + + $compiler->addDebugInfo($parent); + if ($parent instanceof ConstantExpression) { + $compiler + ->write('$this->parent = $this->load(') + ->subcompile($parent) + ->raw(', ') + ->repr($parent->getTemplateLine()) + ->raw(");\n") + ; + } + $compiler->write('yield from '); + + if ($parent instanceof ConstantExpression) { + $compiler->raw('$this->parent'); + } else { + $compiler->raw('$this->getParent($context)'); + } + $compiler->raw("->unwrap()->yield(\$context, array_merge(\$this->blocks, \$blocks));\n"); + } + + $compiler->subcompile($this->getNode('display_end')); + + if (!$this->hasNode('parent')) { + $compiler->write("yield from [];\n"); + } + + $compiler + ->outdent() + ->write("}\n\n") + ; + } + + /** + * @return void + */ + protected function compileClassFooter(Compiler $compiler) + { + $compiler + ->subcompile($this->getNode('class_end')) + ->outdent() + ->write("}\n") + ; + } + + /** + * @return void + */ + protected function compileMacros(Compiler $compiler) + { + $compiler->subcompile($this->getNode('macros')); + } + + /** + * @return void + */ + protected function compileGetTemplateName(Compiler $compiler) + { + $compiler + ->write("/**\n") + ->write(" * @codeCoverageIgnore\n") + ->write(" */\n") + ->write("public function getTemplateName(): string\n", "{\n") + ->indent() + ->write('return ') + ->repr($this->getSourceContext()->getName()) + ->raw(";\n") + ->outdent() + ->write("}\n\n") + ; + } + + /** + * @return void + */ + protected function compileIsTraitable(Compiler $compiler) + { + // A template can be used as a trait if: + // * it has no parent + // * it has no macros + // * it has no body + // + // Put another way, a template can be used as a trait if it + // only contains blocks and use statements. + $traitable = !$this->hasNode('parent') && 0 === \count($this->getNode('macros')); + if ($traitable) { + if ($this->getNode('body') instanceof BodyNode) { + $nodes = $this->getNode('body')->getNode('0'); + } else { + $nodes = $this->getNode('body'); + } + + if (!\count($nodes)) { + $nodes = new Nodes([$nodes]); + } + + foreach ($nodes as $node) { + if (!\count($node)) { + continue; + } + + $traitable = false; + break; + } + } + + if ($traitable) { + return; + } + + $compiler + ->write("/**\n") + ->write(" * @codeCoverageIgnore\n") + ->write(" */\n") + ->write("public function isTraitable(): bool\n", "{\n") + ->indent() + ->write("return false;\n") + ->outdent() + ->write("}\n\n") + ; + } + + /** + * @return void + */ + protected function compileDebugInfo(Compiler $compiler) + { + $compiler + ->write("/**\n") + ->write(" * @codeCoverageIgnore\n") + ->write(" */\n") + ->write("public function getDebugInfo(): array\n", "{\n") + ->indent() + ->write(\sprintf("return %s;\n", str_replace("\n", '', var_export(array_reverse($compiler->getDebugInfo(), true), true)))) + ->outdent() + ->write("}\n\n") + ; + } + + /** + * @return void + */ + protected function compileGetSourceContext(Compiler $compiler) + { + $compiler + ->write("public function getSourceContext(): Source\n", "{\n") + ->indent() + ->write('return new Source(') + ->string($compiler->getEnvironment()->isDebug() ? $this->getSourceContext()->getCode() : '') + ->raw(', ') + ->string($this->getSourceContext()->getName()) + ->raw(', ') + ->string($this->getSourceContext()->getPath()) + ->raw(");\n") + ->outdent() + ->write("}\n") + ; + } +} diff --git a/novaconium/vendor/twig/src/Node/NameDeprecation.php b/novaconium/vendor/twig/src/Node/NameDeprecation.php new file mode 100644 index 0000000..63ab285 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/NameDeprecation.php @@ -0,0 +1,46 @@ + + */ +class NameDeprecation +{ + private $package; + private $version; + private $newName; + + public function __construct(string $package = '', string $version = '', string $newName = '') + { + $this->package = $package; + $this->version = $version; + $this->newName = $newName; + } + + public function getPackage(): string + { + return $this->package; + } + + public function getVersion(): string + { + return $this->version; + } + + public function getNewName(): string + { + return $this->newName; + } +} diff --git a/novaconium/vendor/twig/src/Node/Node.php b/novaconium/vendor/twig/src/Node/Node.php new file mode 100644 index 0000000..dcf912c --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Node.php @@ -0,0 +1,294 @@ + + * + * @implements \IteratorAggregate + */ +#[YieldReady] +class Node implements \Countable, \IteratorAggregate +{ + /** + * @var array + */ + protected $nodes; + protected $attributes; + protected $lineno; + protected $tag; + + private $sourceContext; + /** @var array */ + private $nodeNameDeprecations = []; + /** @var array */ + private $attributeNameDeprecations = []; + + /** + * @param array $nodes An array of named nodes + * @param array $attributes An array of attributes (should not be nodes) + * @param int $lineno The line number + */ + public function __construct(array $nodes = [], array $attributes = [], int $lineno = 0) + { + if (self::class === static::class) { + trigger_deprecation('twig/twig', '3.15', \sprintf('Instantiating "%s" directly is deprecated; the class will become abstract in 4.0.', self::class)); + } + + foreach ($nodes as $name => $node) { + if (!$node instanceof self) { + throw new \InvalidArgumentException(\sprintf('Using "%s" for the value of node "%s" of "%s" is not supported. You must pass a \Twig\Node\Node instance.', get_debug_type($node), $name, static::class)); + } + } + $this->nodes = $nodes; + $this->attributes = $attributes; + $this->lineno = $lineno; + + if (\func_num_args() > 3) { + trigger_deprecation('twig/twig', '3.12', \sprintf('The "tag" constructor argument of the "%s" class is deprecated and ignored (check which TokenParser class set it to "%s"), the tag is now automatically set by the Parser when needed.', static::class, func_get_arg(3) ?: 'null')); + } + } + + public function __toString(): string + { + $repr = static::class; + + if ($this->tag) { + $repr .= \sprintf("\n tag: %s", $this->tag); + } + + $attributes = []; + foreach ($this->attributes as $name => $value) { + if (\is_callable($value)) { + $v = '\Closure'; + } elseif ($value instanceof \Stringable) { + $v = (string) $value; + } else { + $v = str_replace("\n", '', var_export($value, true)); + } + $attributes[] = \sprintf('%s: %s', $name, $v); + } + + if ($attributes) { + $repr .= \sprintf("\n attributes:\n %s", implode("\n ", $attributes)); + } + + if (\count($this->nodes)) { + $repr .= "\n nodes:"; + foreach ($this->nodes as $name => $node) { + $len = \strlen($name) + 6; + $noderepr = []; + foreach (explode("\n", (string) $node) as $line) { + $noderepr[] = str_repeat(' ', $len).$line; + } + + $repr .= \sprintf("\n %s: %s", $name, ltrim(implode("\n", $noderepr))); + } + } + + return $repr; + } + + public function __clone() + { + foreach ($this->nodes as $name => $node) { + $this->nodes[$name] = clone $node; + } + } + + /** + * @return void + */ + public function compile(Compiler $compiler) + { + foreach ($this->nodes as $node) { + $compiler->subcompile($node); + } + } + + public function getTemplateLine(): int + { + return $this->lineno; + } + + public function getNodeTag(): ?string + { + return $this->tag; + } + + /** + * @internal + */ + public function setNodeTag(string $tag): void + { + if ($this->tag) { + throw new \LogicException('The tag of a node can only be set once.'); + } + + $this->tag = $tag; + } + + public function hasAttribute(string $name): bool + { + return \array_key_exists($name, $this->attributes); + } + + public function getAttribute(string $name) + { + if (!\array_key_exists($name, $this->attributes)) { + throw new \LogicException(\sprintf('Attribute "%s" does not exist for Node "%s".', $name, static::class)); + } + + $triggerDeprecation = \func_num_args() > 1 ? func_get_arg(1) : true; + if ($triggerDeprecation && isset($this->attributeNameDeprecations[$name])) { + $dep = $this->attributeNameDeprecations[$name]; + if ($dep->getNewName()) { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Getting attribute "%s" on a "%s" class is deprecated, get the "%s" attribute instead.', $name, static::class, $dep->getNewName()); + } else { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Getting attribute "%s" on a "%s" class is deprecated.', $name, static::class); + } + } + + return $this->attributes[$name]; + } + + public function setAttribute(string $name, $value): void + { + $triggerDeprecation = \func_num_args() > 2 ? func_get_arg(2) : true; + if ($triggerDeprecation && isset($this->attributeNameDeprecations[$name])) { + $dep = $this->attributeNameDeprecations[$name]; + if ($dep->getNewName()) { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Setting attribute "%s" on a "%s" class is deprecated, set the "%s" attribute instead.', $name, static::class, $dep->getNewName()); + } else { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Setting attribute "%s" on a "%s" class is deprecated.', $name, static::class); + } + } + + $this->attributes[$name] = $value; + } + + public function deprecateAttribute(string $name, NameDeprecation $dep): void + { + $this->attributeNameDeprecations[$name] = $dep; + } + + public function removeAttribute(string $name): void + { + unset($this->attributes[$name]); + } + + /** + * @param string|int $name + */ + public function hasNode(string $name): bool + { + return isset($this->nodes[$name]); + } + + /** + * @param string|int $name + */ + public function getNode(string $name): self + { + if (!isset($this->nodes[$name])) { + throw new \LogicException(\sprintf('Node "%s" does not exist for Node "%s".', $name, static::class)); + } + + $triggerDeprecation = \func_num_args() > 1 ? func_get_arg(1) : true; + if ($triggerDeprecation && isset($this->nodeNameDeprecations[$name])) { + $dep = $this->nodeNameDeprecations[$name]; + if ($dep->getNewName()) { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Getting node "%s" on a "%s" class is deprecated, get the "%s" node instead.', $name, static::class, $dep->getNewName()); + } else { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Getting node "%s" on a "%s" class is deprecated.', $name, static::class); + } + } + + return $this->nodes[$name]; + } + + /** + * @param string|int $name + */ + public function setNode(string $name, self $node): void + { + $triggerDeprecation = \func_num_args() > 2 ? func_get_arg(2) : true; + if ($triggerDeprecation && isset($this->nodeNameDeprecations[$name])) { + $dep = $this->nodeNameDeprecations[$name]; + if ($dep->getNewName()) { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Setting node "%s" on a "%s" class is deprecated, set the "%s" node instead.', $name, static::class, $dep->getNewName()); + } else { + trigger_deprecation($dep->getPackage(), $dep->getVersion(), 'Setting node "%s" on a "%s" class is deprecated.', $name, static::class); + } + } + + if (null !== $this->sourceContext) { + $node->setSourceContext($this->sourceContext); + } + $this->nodes[$name] = $node; + } + + /** + * @param string|int $name + */ + public function removeNode(string $name): void + { + unset($this->nodes[$name]); + } + + /** + * @param string|int $name + */ + public function deprecateNode(string $name, NameDeprecation $dep): void + { + $this->nodeNameDeprecations[$name] = $dep; + } + + /** + * @return int + */ + #[\ReturnTypeWillChange] + public function count() + { + return \count($this->nodes); + } + + public function getIterator(): \Traversable + { + return new \ArrayIterator($this->nodes); + } + + public function getTemplateName(): ?string + { + return $this->sourceContext ? $this->sourceContext->getName() : null; + } + + public function setSourceContext(Source $source): void + { + $this->sourceContext = $source; + foreach ($this->nodes as $node) { + $node->setSourceContext($source); + } + } + + public function getSourceContext(): ?Source + { + return $this->sourceContext; + } +} diff --git a/novaconium/vendor/twig/src/Node/NodeCaptureInterface.php b/novaconium/vendor/twig/src/Node/NodeCaptureInterface.php new file mode 100644 index 0000000..9fb6a0c --- /dev/null +++ b/novaconium/vendor/twig/src/Node/NodeCaptureInterface.php @@ -0,0 +1,21 @@ + + */ +interface NodeCaptureInterface +{ +} diff --git a/novaconium/vendor/twig/src/Node/NodeOutputInterface.php b/novaconium/vendor/twig/src/Node/NodeOutputInterface.php new file mode 100644 index 0000000..5e35b40 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/NodeOutputInterface.php @@ -0,0 +1,21 @@ + + */ +interface NodeOutputInterface +{ +} diff --git a/novaconium/vendor/twig/src/Node/Nodes.php b/novaconium/vendor/twig/src/Node/Nodes.php new file mode 100644 index 0000000..bd67053 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/Nodes.php @@ -0,0 +1,28 @@ + + */ +#[YieldReady] +final class Nodes extends Node +{ + public function __construct(array $nodes = [], int $lineno = 0) + { + parent::__construct($nodes, [], $lineno); + } +} diff --git a/novaconium/vendor/twig/src/Node/PrintNode.php b/novaconium/vendor/twig/src/Node/PrintNode.php new file mode 100644 index 0000000..0eec95d --- /dev/null +++ b/novaconium/vendor/twig/src/Node/PrintNode.php @@ -0,0 +1,69 @@ + + */ +#[YieldReady] +class PrintNode extends Node implements NodeOutputInterface, CoercesChildrenToStringInterface +{ + public function __construct(AbstractExpression $expr, int $lineno) + { + parent::__construct(['expr' => $expr], [], $lineno); + } + + public function compile(Compiler $compiler): void + { + /** @var AbstractExpression */ + $expr = $this->getNode('expr'); + + $compiler->addDebugInfo($this); + + if ($expr->isGenerator()) { + $compiler->write('yield from '); + } else { + $compiler->write('yield '); + if (!$this->isString($expr)) { + $compiler->raw('(string) '); + } + } + + $compiler + ->subcompile($expr) + ->raw(";\n") + ; + } + + public function getStringCoercedChildNames(): array + { + return ['expr']; + } + + private function isString(AbstractExpression $expr): bool + { + if ($expr instanceof ReturnStringInterface) { + return true; + } + + return $expr instanceof ConstantExpression && !$expr->isDefinedTestEnabled() && \is_string($expr->getAttribute('value')); + } +} diff --git a/novaconium/vendor/twig/src/Node/SandboxNode.php b/novaconium/vendor/twig/src/Node/SandboxNode.php new file mode 100644 index 0000000..d51cea4 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/SandboxNode.php @@ -0,0 +1,54 @@ + + */ +#[YieldReady] +class SandboxNode extends Node +{ + public function __construct(Node $body, int $lineno) + { + parent::__construct(['body' => $body], [], $lineno); + } + + public function compile(Compiler $compiler): void + { + $compiler + ->addDebugInfo($this) + ->write("if (!\$alreadySandboxed = \$this->sandbox->isSandboxed()) {\n") + ->indent() + ->write("\$this->sandbox->enableSandbox();\n") + ->outdent() + ->write("}\n") + ->write("try {\n") + ->indent() + ->subcompile($this->getNode('body')) + ->outdent() + ->write("} finally {\n") + ->indent() + ->write("if (!\$alreadySandboxed) {\n") + ->indent() + ->write("\$this->sandbox->disableSandbox();\n") + ->outdent() + ->write("}\n") + ->outdent() + ->write("}\n") + ; + } +} diff --git a/novaconium/vendor/twig/src/Node/SetNode.php b/novaconium/vendor/twig/src/Node/SetNode.php new file mode 100644 index 0000000..7b063b0 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/SetNode.php @@ -0,0 +1,115 @@ + + */ +#[YieldReady] +class SetNode extends Node implements NodeCaptureInterface +{ + public function __construct(bool $capture, Node $names, Node $values, int $lineno) + { + /* + * Optimizes the node when capture is used for a large block of text. + * + * {% set foo %}foo{% endset %} is compiled to $context['foo'] = new Twig\Markup("foo"); + */ + $safe = false; + if ($capture) { + $safe = true; + // Node::class === get_class($values) should be removed in Twig 4.0 + if (($values instanceof Nodes || Node::class === $values::class) && !\count($values)) { + $values = new ConstantExpression('', $values->getTemplateLine()); + $capture = false; + } elseif ($values instanceof TextNode) { + $values = new ConstantExpression($values->getAttribute('data'), $values->getTemplateLine()); + $capture = false; + } elseif ($values instanceof PrintNode && $values->getNode('expr') instanceof ConstantExpression) { + $values = $values->getNode('expr'); + $capture = false; + } else { + $values = new CaptureNode($values, $values->getTemplateLine()); + } + } + + parent::__construct(['names' => $names, 'values' => $values], ['capture' => $capture, 'safe' => $safe], $lineno); + } + + public function compile(Compiler $compiler): void + { + $compiler->addDebugInfo($this); + + if (\count($this->getNode('names')) > 1) { + $compiler->write('['); + foreach ($this->getNode('names') as $idx => $node) { + if ($idx) { + $compiler->raw(', '); + } + + $compiler->subcompile($node); + } + $compiler->raw(']'); + } else { + $compiler->subcompile($this->getNode('names'), false); + } + $compiler->raw(' = '); + + if ($this->getAttribute('capture')) { + $compiler->subcompile($this->getNode('values')); + } else { + if (\count($this->getNode('names')) > 1) { + $compiler->write('['); + foreach ($this->getNode('values') as $idx => $value) { + if ($idx) { + $compiler->raw(', '); + } + + $compiler->subcompile($value); + } + $compiler->raw(']'); + } else { + if ($this->getAttribute('safe')) { + if ($this->getNode('values') instanceof ConstantExpression) { + if ('' === $this->getNode('values')->getAttribute('value')) { + $compiler->raw('""'); + } else { + $compiler + ->raw('new Markup(') + ->subcompile($this->getNode('values')) + ->raw(', $this->env->getCharset())') + ; + } + } else { + $compiler + ->raw("('' === \$tmp = ") + ->subcompile($this->getNode('values')) + ->raw(") ? '' : new Markup(\$tmp, \$this->env->getCharset())") + ; + } + } else { + $compiler->subcompile($this->getNode('values')); + } + } + + $compiler->raw(';'); + } + + $compiler->raw("\n"); + } +} diff --git a/novaconium/vendor/twig/src/Node/TextNode.php b/novaconium/vendor/twig/src/Node/TextNode.php new file mode 100644 index 0000000..86071da --- /dev/null +++ b/novaconium/vendor/twig/src/Node/TextNode.php @@ -0,0 +1,57 @@ + + */ +#[YieldReady] +class TextNode extends Node implements NodeOutputInterface +{ + public function __construct(string $data, int $lineno) + { + parent::__construct([], ['data' => $data], $lineno); + } + + public function compile(Compiler $compiler): void + { + $compiler->addDebugInfo($this); + + $compiler + ->write('yield ') + ->string($this->getAttribute('data')) + ->raw(";\n") + ; + } + + public function isBlank(): bool + { + if (ctype_space($this->getAttribute('data'))) { + return true; + } + + if (str_contains((string) $this, \chr(0xEF).\chr(0xBB).\chr(0xBF))) { + $t = substr($this->getAttribute('data'), 3); + if ('' === $t || ctype_space($t)) { + return true; + } + } + + return false; + } +} diff --git a/novaconium/vendor/twig/src/Node/TypesNode.php b/novaconium/vendor/twig/src/Node/TypesNode.php new file mode 100644 index 0000000..a182880 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/TypesNode.php @@ -0,0 +1,40 @@ + + */ +#[YieldReady] +class TypesNode extends Node +{ + /** + * @param array $types + */ + public function __construct(array $types, int $lineno) + { + parent::__construct([], ['mapping' => $types], $lineno); + } + + /** + * @return void + */ + public function compile(Compiler $compiler) + { + // Don't compile anything. + } +} diff --git a/novaconium/vendor/twig/src/Node/WithNode.php b/novaconium/vendor/twig/src/Node/WithNode.php new file mode 100644 index 0000000..487e280 --- /dev/null +++ b/novaconium/vendor/twig/src/Node/WithNode.php @@ -0,0 +1,72 @@ + + */ +#[YieldReady] +class WithNode extends Node +{ + public function __construct(Node $body, ?Node $variables, bool $only, int $lineno) + { + $nodes = ['body' => $body]; + if (null !== $variables) { + $nodes['variables'] = $variables; + } + + parent::__construct($nodes, ['only' => $only], $lineno); + } + + public function compile(Compiler $compiler): void + { + $compiler->addDebugInfo($this); + + $parentContextName = $compiler->getVarName(); + + $compiler->write(\sprintf("\$%s = \$context;\n", $parentContextName)); + + if ($this->hasNode('variables')) { + $node = $this->getNode('variables'); + $varsName = $compiler->getVarName(); + $compiler + ->write(\sprintf('$%s = ', $varsName)) + ->subcompile($node) + ->raw(";\n") + ->write(\sprintf("if (!is_iterable(\$%s)) {\n", $varsName)) + ->indent() + ->write("throw new RuntimeError('Variables passed to the \"with\" tag must be a mapping.', ") + ->repr($node->getTemplateLine()) + ->raw(", \$this->getSourceContext());\n") + ->outdent() + ->write("}\n") + ->write(\sprintf("\$%s = CoreExtension::toArray(\$%s);\n", $varsName, $varsName)) + ; + + if ($this->getAttribute('only')) { + $compiler->write("\$context = [];\n"); + } + + $compiler->write(\sprintf("\$context = \$%s + \$context + \$this->env->getGlobals();\n", $varsName)); + } + + $compiler + ->subcompile($this->getNode('body')) + ->write(\sprintf("\$context = \$%s;\n", $parentContextName)) + ; + } +} diff --git a/novaconium/vendor/twig/src/NodeTraverser.php b/novaconium/vendor/twig/src/NodeTraverser.php new file mode 100644 index 0000000..47a2d5c --- /dev/null +++ b/novaconium/vendor/twig/src/NodeTraverser.php @@ -0,0 +1,76 @@ + + */ +final class NodeTraverser +{ + private $env; + private $visitors = []; + + /** + * @param NodeVisitorInterface[] $visitors + */ + public function __construct(Environment $env, array $visitors = []) + { + $this->env = $env; + foreach ($visitors as $visitor) { + $this->addVisitor($visitor); + } + } + + public function addVisitor(NodeVisitorInterface $visitor): void + { + $this->visitors[$visitor->getPriority()][] = $visitor; + } + + /** + * Traverses a node and calls the registered visitors. + */ + public function traverse(Node $node): Node + { + ksort($this->visitors); + foreach ($this->visitors as $visitors) { + foreach ($visitors as $visitor) { + $node = $this->traverseForVisitor($visitor, $node); + } + } + + return $node; + } + + private function traverseForVisitor(NodeVisitorInterface $visitor, Node $node): ?Node + { + $node = $visitor->enterNode($node, $this->env); + + foreach ($node as $k => $n) { + if (null !== $m = $this->traverseForVisitor($visitor, $n)) { + if ($m !== $n) { + $node->setNode($k, $m); + } + } else { + $node->removeNode($k); + } + } + + return $visitor->leaveNode($node, $this->env); + } +} diff --git a/novaconium/vendor/twig/src/NodeVisitor/AbstractNodeVisitor.php b/novaconium/vendor/twig/src/NodeVisitor/AbstractNodeVisitor.php new file mode 100644 index 0000000..38b1ec9 --- /dev/null +++ b/novaconium/vendor/twig/src/NodeVisitor/AbstractNodeVisitor.php @@ -0,0 +1,49 @@ + + * + * @deprecated since Twig 3.9 (to be removed in 4.0) + */ +abstract class AbstractNodeVisitor implements NodeVisitorInterface +{ + final public function enterNode(Node $node, Environment $env): Node + { + return $this->doEnterNode($node, $env); + } + + final public function leaveNode(Node $node, Environment $env): ?Node + { + return $this->doLeaveNode($node, $env); + } + + /** + * Called before child nodes are visited. + * + * @return Node The modified node + */ + abstract protected function doEnterNode(Node $node, Environment $env); + + /** + * Called after child nodes are visited. + * + * @return Node|null The modified node or null if the node must be removed + */ + abstract protected function doLeaveNode(Node $node, Environment $env); +} diff --git a/novaconium/vendor/twig/src/NodeVisitor/CorrectnessNodeVisitor.php b/novaconium/vendor/twig/src/NodeVisitor/CorrectnessNodeVisitor.php new file mode 100644 index 0000000..6fd4e17 --- /dev/null +++ b/novaconium/vendor/twig/src/NodeVisitor/CorrectnessNodeVisitor.php @@ -0,0 +1,223 @@ + + * + * @internal + */ +final class CorrectnessNodeVisitor implements NodeVisitorInterface +{ + private ?\WeakMap $rootNodes = null; + /** + * Stack of the output-wrapping tags ("if", "for", "set", ...) currently open; + * the top one is the nearest tag a "block" definition would be nested under. + * + * @var list + */ + private array $tagStack = []; + private bool $hasParent = false; + private int $blockDepth = 0; + private int $macroDepth = 0; + private int $capturingNodeDepth = 0; + private bool $hasExtends = false; + + public function enterNode(Node $node, Environment $env): Node + { + if ($node instanceof ModuleNode) { + $this->enterModule($node); + + return $node; + } + + $this->enterScope($node); + + if ($node instanceof ConfigNode) { + $this->checkConfigTag($node); + } + + if ($node instanceof BlockReferenceNode) { + $this->checkBlockDefinitionNesting($node); + } + + return $node; + } + + public function leaveNode(Node $node, Environment $env): Node + { + if ($node instanceof ModuleNode) { + $this->resetState(); + + return $node; + } + + $this->leaveScope($node); + + return $node; + } + + public function getPriority(): int + { + return -255; + } + + private function enterModule(ModuleNode $node): void + { + $this->resetState(); + $this->rootNodes = new \WeakMap(); + $this->hasParent = $node->hasNode('parent'); + + foreach ($this->getRootNodes($node) as $n) { + if ($this->hasParent && !$this->isEmptyOutputNode($n)) { + throw new SyntaxError('A template that extends another one cannot include content outside Twig blocks. Did you forget to put the content inside a {% block %} tag?', $n->getTemplateLine(), $n->getSourceContext()); + } + $this->rootNodes[$n] = true; + } + } + + private function resetState(): void + { + $this->rootNodes = null; + $this->tagStack = []; + $this->hasParent = false; + $this->blockDepth = 0; + $this->macroDepth = 0; + $this->capturingNodeDepth = 0; + $this->hasExtends = false; + } + + /** + * @return iterable + */ + private function getRootNodes(ModuleNode $node): iterable + { + $body = $node->getNode('body')->getNode('0'); + + // Parser::subparse() does not wrap the parsed nodes when there is only one, + // so $body can be a single "real" node instead of a Nodes container; in that + // case it is the only root node and must not be iterated over its children. + return $body instanceof Nodes || Node::class === $body::class ? $body : [$body]; + } + + private function enterScope(Node $node): void + { + if ($node instanceof NodeCaptureInterface) { + ++$this->capturingNodeDepth; + } + + if ($node instanceof BlockNode) { + ++$this->blockDepth; + } elseif ($node instanceof MacroNode) { + ++$this->macroDepth; + } elseif ($node->getNodeTag() && !$node instanceof BlockReferenceNode) { + $this->tagStack[] = $node; + } + } + + private function leaveScope(Node $node): void + { + if ($node instanceof NodeCaptureInterface) { + --$this->capturingNodeDepth; + } + + if ($node instanceof BlockNode) { + --$this->blockDepth; + } elseif ($node instanceof MacroNode) { + --$this->macroDepth; + } elseif ($node->getNodeTag() && !$node instanceof BlockReferenceNode) { + array_pop($this->tagStack); + } + } + + private function checkConfigTag(ConfigNode $node): void + { + if ('extends' === $node->getNodeTag()) { + $this->checkExtendsTag($node); + } + + if (!isset($this->rootNodes[$node])) { + trigger_deprecation('twig/twig', '3.27', 'Using the "%s" tag outside the root of a template is deprecated in %s at line %d.', $node->getNodeTag(), $node->getSourceContext()->getName(), $node->getTemplateLine()); + } + } + + private function checkExtendsTag(ConfigNode $node): void + { + // "extends" inside a "block" or a "macro" has always been a hard error; keep it + if ($this->blockDepth) { + throw new SyntaxError('Cannot use "extend" in a block.', $node->getTemplateLine(), $node->getSourceContext()); + } + if ($this->macroDepth) { + throw new SyntaxError('Cannot use "extend" in a macro.', $node->getTemplateLine(), $node->getSourceContext()); + } + if ($this->hasExtends) { + throw new SyntaxError('Multiple extends tags are forbidden.', $node->getTemplateLine(), $node->getSourceContext()); + } + + $this->hasExtends = true; + } + + private function checkBlockDefinitionNesting(BlockReferenceNode $node): void + { + // A "block" definition nested under an output-wrapping tag is registered globally + // regardless of that tag, so the nesting is misleading. This only matters at the root + // of a child template's body: once inside a block, a macro, an output capture, or in + // a standalone template, the block is rendered in place and behaves like any other. + if (!$this->hasParent || $this->blockDepth || $this->macroDepth || $this->capturingNodeDepth || !$this->tagStack) { + return; + } + + $tag = $this->tagStack[array_key_last($this->tagStack)]; + throw new SyntaxError(\sprintf('A "block" tag cannot be under a "%s" tag (line %d).', $tag->getNodeTag(), $tag->getTemplateLine()), $node->getTemplateLine(), $node->getSourceContext()); + } + + /** + * Returns true if the node never outputs anything or if the output is empty. + */ + private function isEmptyOutputNode(Node $node): bool + { + if ($node instanceof NodeCaptureInterface) { + // a "block" tag in such a node will serve as a block definition AND be displayed in place as well + return true; + } + + // Can the text be considered "empty" (only whitespace)? + if ($node instanceof TextNode) { + return $node->isBlank(); + } + + if (!$node instanceof BlockReferenceNode && $node instanceof NodeOutputInterface) { + return false; + } + + foreach ($node as $n) { + if (!$this->isEmptyOutputNode($n)) { + return false; + } + } + + return true; + } +} diff --git a/novaconium/vendor/twig/src/NodeVisitor/EscaperNodeVisitor.php b/novaconium/vendor/twig/src/NodeVisitor/EscaperNodeVisitor.php new file mode 100644 index 0000000..a9f8297 --- /dev/null +++ b/novaconium/vendor/twig/src/NodeVisitor/EscaperNodeVisitor.php @@ -0,0 +1,185 @@ + + * + * @internal + */ +final class EscaperNodeVisitor implements NodeVisitorInterface +{ + private $statusStack = []; + private $blocks = []; + private $safeAnalysis; + private $traverser; + private $defaultStrategy = false; + private $safeVars = []; + + public function __construct() + { + $this->safeAnalysis = new SafeAnalysisNodeVisitor(); + } + + public function enterNode(Node $node, Environment $env): Node + { + if ($node instanceof ModuleNode) { + if ($env->hasExtension(EscaperExtension::class) && $defaultStrategy = $env->getExtension(EscaperExtension::class)->getDefaultStrategy($node->getTemplateName())) { + $this->defaultStrategy = $defaultStrategy; + } + $this->safeVars = []; + $this->blocks = []; + } elseif ($node instanceof AutoEscapeNode) { + $this->statusStack[] = $node->getAttribute('value'); + } elseif ($node instanceof BlockNode) { + $this->statusStack[] = $this->blocks[$node->getAttribute('name')] ?? $this->needEscaping(); + } elseif ($node instanceof ImportNode) { + $this->safeVars[] = $node->getNode('var')->getNode('var')->getAttribute('name'); + } + + return $node; + } + + public function leaveNode(Node $node, Environment $env): ?Node + { + if ($node instanceof ModuleNode) { + $this->defaultStrategy = false; + $this->safeVars = []; + $this->blocks = []; + } elseif ($node instanceof FilterExpression) { + return $this->preEscapeFilterNode($node, $env); + } elseif ($node instanceof PrintNode && false !== $type = $this->needEscaping()) { + $expression = $node->getNode('expr'); + if ($expression instanceof OperatorEscapeInterface) { + $this->escapeConditional($expression, $env, $type); + } else { + $node->setNode('expr', $this->escapeExpression($expression, $env, $type)); + } + + return $node; + } + + if ($node instanceof AutoEscapeNode || $node instanceof BlockNode) { + array_pop($this->statusStack); + } elseif ($node instanceof BlockReferenceNode) { + $this->blocks[$node->getAttribute('name')] = $this->needEscaping(); + } + + return $node; + } + + /** + * @param AbstractExpression&OperatorEscapeInterface $expression + */ + private function escapeConditional($expression, Environment $env, string $type): void + { + foreach ($expression->getOperandNamesToEscape() as $name) { + /** @var AbstractExpression $operand */ + $operand = $expression->getNode($name); + if ($operand instanceof OperatorEscapeInterface) { + $this->escapeConditional($operand, $env, $type); + } else { + $expression->setNode($name, $this->escapeExpression($operand, $env, $type)); + } + } + } + + private function escapeExpression(AbstractExpression $expression, Environment $env, string $type): AbstractExpression + { + return $this->isSafeFor($type, $expression, $env) ? $expression : $this->getEscaperFilter($env, $type, $expression); + } + + private function preEscapeFilterNode(FilterExpression $filter, Environment $env): FilterExpression + { + if ($filter->hasAttribute('twig_callable')) { + $type = $filter->getAttribute('twig_callable')->getPreEscape(); + } else { + // legacy + $name = $filter->getNode('filter', false)->getAttribute('value'); + $type = $env->getFilter($name)->getPreEscape(); + } + + if (null === $type) { + return $filter; + } + + /** @var AbstractExpression $node */ + $node = $filter->getNode('node'); + if ($this->isSafeFor($type, $node, $env)) { + return $filter; + } + + $filter->setNode('node', $this->getEscaperFilter($env, $type, $node)); + + return $filter; + } + + private function isSafeFor(string $type, AbstractExpression $expression, Environment $env): bool + { + $safe = $this->safeAnalysis->getSafe($expression); + + if (!$safe) { + if (null === $this->traverser) { + $this->traverser = new NodeTraverser($env, [$this->safeAnalysis]); + } + + $this->safeAnalysis->setSafeVars($this->safeVars); + + $this->traverser->traverse($expression); + $safe = $this->safeAnalysis->getSafe($expression); + } + + return \in_array($type, $safe, true) || \in_array('all', $safe, true); + } + + /** + * @return string|false + */ + private function needEscaping(): string|bool + { + if (\count($this->statusStack)) { + return $this->statusStack[\count($this->statusStack) - 1]; + } + + return $this->defaultStrategy ?: false; + } + + private function getEscaperFilter(Environment $env, string $type, AbstractExpression $node): FilterExpression + { + $line = $node->getTemplateLine(); + $filter = $env->getFilter('escape'); + $args = new Nodes([new ConstantExpression($type, $line), new ConstantExpression(null, $line), new ConstantExpression(true, $line)]); + + return new FilterExpression($node, $filter, $args, $line); + } + + public function getPriority(): int + { + return 0; + } +} diff --git a/novaconium/vendor/twig/src/NodeVisitor/NodeVisitorInterface.php b/novaconium/vendor/twig/src/NodeVisitor/NodeVisitorInterface.php new file mode 100644 index 0000000..59e836d --- /dev/null +++ b/novaconium/vendor/twig/src/NodeVisitor/NodeVisitorInterface.php @@ -0,0 +1,46 @@ + + */ +interface NodeVisitorInterface +{ + /** + * Called before child nodes are visited. + * + * @return Node The modified node + */ + public function enterNode(Node $node, Environment $env): Node; + + /** + * Called after child nodes are visited. + * + * @return Node|null The modified node or null if the node must be removed + */ + public function leaveNode(Node $node, Environment $env): ?Node; + + /** + * Returns the priority for this visitor. + * + * Priority should be between -10 and 10 (0 is the default). + * + * @return int The priority level + */ + public function getPriority(); +} diff --git a/novaconium/vendor/twig/src/NodeVisitor/OptimizerNodeVisitor.php b/novaconium/vendor/twig/src/NodeVisitor/OptimizerNodeVisitor.php new file mode 100644 index 0000000..b778ba4 --- /dev/null +++ b/novaconium/vendor/twig/src/NodeVisitor/OptimizerNodeVisitor.php @@ -0,0 +1,213 @@ + + * + * @internal + */ +final class OptimizerNodeVisitor implements NodeVisitorInterface +{ + public const OPTIMIZE_ALL = -1; + public const OPTIMIZE_NONE = 0; + public const OPTIMIZE_FOR = 2; + public const OPTIMIZE_RAW_FILTER = 4; + public const OPTIMIZE_TEXT_NODES = 8; + + private $loops = []; + private $loopsTargets = []; + + /** + * @param int $optimizers The optimizer mode + */ + public function __construct( + private int $optimizers = -1, + ) { + if ($optimizers > (self::OPTIMIZE_FOR | self::OPTIMIZE_RAW_FILTER | self::OPTIMIZE_TEXT_NODES)) { + throw new \InvalidArgumentException(\sprintf('Optimizer mode "%s" is not valid.', $optimizers)); + } + + if (-1 !== $optimizers && self::OPTIMIZE_RAW_FILTER === (self::OPTIMIZE_RAW_FILTER & $optimizers)) { + trigger_deprecation('twig/twig', '3.11', 'The "Twig\NodeVisitor\OptimizerNodeVisitor::OPTIMIZE_RAW_FILTER" option is deprecated and does nothing.'); + } + + if (-1 !== $optimizers && self::OPTIMIZE_TEXT_NODES === (self::OPTIMIZE_TEXT_NODES & $optimizers)) { + trigger_deprecation('twig/twig', '3.12', 'The "Twig\NodeVisitor\OptimizerNodeVisitor::OPTIMIZE_TEXT_NODES" option is deprecated and does nothing.'); + } + } + + public function enterNode(Node $node, Environment $env): Node + { + if (self::OPTIMIZE_FOR === (self::OPTIMIZE_FOR & $this->optimizers)) { + $this->enterOptimizeFor($node); + } + + return $node; + } + + public function leaveNode(Node $node, Environment $env): ?Node + { + if (self::OPTIMIZE_FOR === (self::OPTIMIZE_FOR & $this->optimizers)) { + $this->leaveOptimizeFor($node); + } + + $node = $this->optimizePrintNode($node); + + return $node; + } + + /** + * Optimizes print nodes. + * + * It replaces: + * + * * "echo $this->render(Parent)Block()" with "$this->display(Parent)Block()" + */ + private function optimizePrintNode(Node $node): Node + { + if (!$node instanceof PrintNode) { + return $node; + } + + $exprNode = $node->getNode('expr'); + + if ($exprNode instanceof ConstantExpression && \is_string($exprNode->getAttribute('value'))) { + return new TextNode($exprNode->getAttribute('value'), $exprNode->getTemplateLine()); + } + + if ( + $exprNode instanceof BlockReferenceExpression + || $exprNode instanceof ParentExpression + ) { + $exprNode->setAttribute('output', true); + + return $exprNode; + } + + return $node; + } + + /** + * Optimizes "for" tag by removing the "loop" variable creation whenever possible. + */ + private function enterOptimizeFor(Node $node): void + { + if ($node instanceof ForNode) { + // disable the loop variable by default + $node->setAttribute('with_loop', false); + array_unshift($this->loops, $node); + array_unshift($this->loopsTargets, $node->getNode('value_target')->getAttribute('name')); + array_unshift($this->loopsTargets, $node->getNode('key_target')->getAttribute('name')); + } elseif (!$this->loops) { + // we are outside a loop + return; + } + + // when do we need to add the loop variable back? + + // the loop variable is referenced for the current loop + elseif ($node instanceof ContextVariable && 'loop' === $node->getAttribute('name')) { + $node->setAttribute('always_defined', true); + $this->addLoopToCurrent(); + } + + // optimize access to loop targets + elseif ($node instanceof ContextVariable && \in_array($node->getAttribute('name'), $this->loopsTargets, true)) { + $node->setAttribute('always_defined', true); + } + + // block reference + elseif ($node instanceof BlockReferenceNode || $node instanceof BlockReferenceExpression) { + $this->addLoopToCurrent(); + } + + // include without the only attribute + elseif ($node instanceof IncludeNode && !$node->getAttribute('only')) { + $this->addLoopToAll(); + } + + // include function without the with_context=false parameter + elseif ($node instanceof FunctionExpression + && 'include' === $node->getAttribute('name') + && (!$node->getNode('arguments')->hasNode('with_context') + || false !== $node->getNode('arguments')->getNode('with_context')->getAttribute('value') + ) + ) { + $this->addLoopToAll(); + } + + // the loop variable is referenced via an attribute + elseif ($node instanceof GetAttrExpression + && (!$node->getNode('attribute') instanceof ConstantExpression + || 'parent' === $node->getNode('attribute')->getAttribute('value') + ) + && (true === $this->loops[0]->getAttribute('with_loop') + || ($node->getNode('node') instanceof ContextVariable + && 'loop' === $node->getNode('node')->getAttribute('name') + ) + ) + ) { + $this->addLoopToAll(); + } + } + + /** + * Optimizes "for" tag by removing the "loop" variable creation whenever possible. + */ + private function leaveOptimizeFor(Node $node): void + { + if ($node instanceof ForNode) { + array_shift($this->loops); + array_shift($this->loopsTargets); + array_shift($this->loopsTargets); + } + } + + private function addLoopToCurrent(): void + { + $this->loops[0]->setAttribute('with_loop', true); + } + + private function addLoopToAll(): void + { + foreach ($this->loops as $loop) { + $loop->setAttribute('with_loop', true); + } + } + + public function getPriority(): int + { + return 255; + } +} diff --git a/novaconium/vendor/twig/src/NodeVisitor/SafeAnalysisNodeVisitor.php b/novaconium/vendor/twig/src/NodeVisitor/SafeAnalysisNodeVisitor.php new file mode 100644 index 0000000..b1aea0f --- /dev/null +++ b/novaconium/vendor/twig/src/NodeVisitor/SafeAnalysisNodeVisitor.php @@ -0,0 +1,185 @@ +safeVars = $safeVars; + } + + /** + * @return array + */ + public function getSafe(Node $node) + { + $hash = spl_object_id($node); + if (!isset($this->data[$hash])) { + return []; + } + + foreach ($this->data[$hash] as $bucket) { + if ($bucket['key'] !== $node) { + continue; + } + + if (\in_array('html_attr', $bucket['value'], true)) { + $bucket['value'][] = 'html'; + $bucket['value'][] = 'html_attr_relaxed'; + } + + if (\in_array('html_attr_relaxed', $bucket['value'], true)) { + $bucket['value'][] = 'html'; + } + + return $bucket['value']; + } + + return []; + } + + private function setSafe(Node $node, array $safe): void + { + $hash = spl_object_id($node); + if (isset($this->data[$hash])) { + foreach ($this->data[$hash] as &$bucket) { + if ($bucket['key'] === $node) { + $bucket['value'] = $safe; + + return; + } + } + } + $this->data[$hash][] = [ + 'key' => $node, + 'value' => $safe, + ]; + } + + public function enterNode(Node $node, Environment $env): Node + { + return $node; + } + + public function leaveNode(Node $node, Environment $env): ?Node + { + if ($node instanceof ConstantExpression) { + // constants are marked safe for all + $this->setSafe($node, ['all']); + } elseif ($node instanceof BlockReferenceExpression) { + // blocks are safe by definition + $this->setSafe($node, ['all']); + } elseif ($node instanceof ParentExpression) { + // parent block is safe by definition + $this->setSafe($node, ['all']); + } elseif ($node instanceof OperatorEscapeInterface) { + // intersect safeness of operands + $operands = $node->getOperandNamesToEscape(); + if (2 < \count($operands)) { + throw new \LogicException(\sprintf('Operators with more than 2 operands are not supported yet, got %d.', \count($operands))); + } elseif (2 === \count($operands)) { + $safe = $this->intersectSafe($this->getSafe($node->getNode($operands[0])), $this->getSafe($node->getNode($operands[1]))); + $this->setSafe($node, $safe); + } + } elseif ($node instanceof FilterExpression) { + // filter expression is safe when the filter is safe + if ($node->hasAttribute('twig_callable')) { + $filter = $node->getAttribute('twig_callable'); + } else { + // legacy + $filter = $env->getFilter($node->getAttribute('name')); + } + + if ($filter) { + $safe = $filter->getSafe($node->getNode('arguments')); + if (null === $safe) { + trigger_deprecation('twig/twig', '3.16', 'The "%s::getSafe()" method should not return "null" anymore, return "[]" instead.', $filter::class); + $safe = []; + } + + if (!$safe) { + $safe = $this->intersectSafe($this->getSafe($node->getNode('node')), $filter->getPreservesSafety()); + } + $this->setSafe($node, $safe); + } + } elseif ($node instanceof FunctionExpression) { + // function expression is safe when the function is safe + if ($node->hasAttribute('twig_callable')) { + $function = $node->getAttribute('twig_callable'); + } else { + // legacy + $function = $env->getFunction($node->getAttribute('name')); + } + + if ($function) { + $safe = $function->getSafe($node->getNode('arguments')); + if (null === $safe) { + trigger_deprecation('twig/twig', '3.16', 'The "%s::getSafe()" method should not return "null" anymore, return "[]" instead.', $function::class); + $safe = []; + } + $this->setSafe($node, $safe); + } + } elseif ($node instanceof MethodCallExpression || $node instanceof MacroReferenceExpression) { + // all macro calls are safe + $this->setSafe($node, ['all']); + } elseif ($node instanceof GetAttrExpression && $node->getNode('node') instanceof ContextVariable) { + $name = $node->getNode('node')->getAttribute('name'); + if (\in_array($name, $this->safeVars, true)) { + $this->setSafe($node, ['all']); + } + } + + return $node; + } + + private function intersectSafe(array $a, array $b): array + { + if (!$a || !$b) { + return []; + } + + if (\in_array('all', $a, true)) { + return $b; + } + + if (\in_array('all', $b, true)) { + return $a; + } + + return array_intersect($a, $b); + } + + public function getPriority(): int + { + return 0; + } +} diff --git a/novaconium/vendor/twig/src/NodeVisitor/SandboxNodeVisitor.php b/novaconium/vendor/twig/src/NodeVisitor/SandboxNodeVisitor.php new file mode 100644 index 0000000..dba49c8 --- /dev/null +++ b/novaconium/vendor/twig/src/NodeVisitor/SandboxNodeVisitor.php @@ -0,0 +1,295 @@ + + * + * @internal + */ +final class SandboxNodeVisitor implements NodeVisitorInterface +{ + private $inAModule = false; + /** @var array */ + private $tags; + /** @var array */ + private $filters; + /** @var array */ + private $functions; + /** @var array */ + private $tests; + + public function enterNode(Node $node, Environment $env): Node + { + if ($node instanceof ModuleNode) { + $this->inAModule = true; + $this->tags = []; + $this->filters = []; + $this->functions = []; + $this->tests = []; + } elseif ($this->inAModule) { + // look for tags + if ($node->getNodeTag() && !isset($this->tags[$node->getNodeTag()]) && !$this->isTagAlwaysAllowedInSandbox($env, $node->getNodeTag())) { + $this->tags[$node->getNodeTag()] = $node->getTemplateLine(); + } + + // look for filters + if ($node instanceof FilterExpression && !isset($this->filters[$name = $node->getAttribute('name')]) && !$this->isFilterAlwaysAllowedInSandbox($env, $node)) { + $this->filters[$name] = $node->getTemplateLine(); + } + + // look for functions + if ($node instanceof FunctionExpression && !isset($this->functions[$name = $node->getAttribute('name')]) && !$this->isFunctionAlwaysAllowedInSandbox($env, $node)) { + $this->functions[$name] = $node->getTemplateLine(); + } + + // look for tests + if ($node instanceof TestExpression && !isset($this->tests[$name = $node->getAttribute('name')]) && !$this->isTestAlwaysAllowedInSandbox($env, $node)) { + $this->tests[$name] = $node->getTemplateLine(); + } + + // look for functions whose parser callable replaced the FunctionExpression + // with a specialized node (e.g. `parent`, `block`, `attribute`); the + // original function name was stashed by FunctionExpressionParser. + if ($node->hasAttribute('sandboxed_function_name')) { + $name = $node->getAttribute('sandboxed_function_name'); + if (!isset($this->functions[$name]) && !$this->isSandboxedFunctionAlwaysAllowedInSandbox($env, $node, $name)) { + $this->functions[$name] = $node->getTemplateLine(); + } + } + + // the .. operator is equivalent to the range() function + if ($node instanceof RangeBinary && !isset($this->functions['range']) && !$this->isFunctionNameAlwaysAllowedInSandbox($env, 'range')) { + $this->functions['range'] = $node->getTemplateLine(); + } + } + + // wrap children that the node itself will string-coerce at runtime; + // applies to ModuleNode (`parent` slot for {% extends %}) too + if ($this->inAModule && $node instanceof CoercesChildrenToStringInterface) { + $params = CallableParameters::fromNode($node, $env); + foreach ($node->getStringCoercedChildNames() as $childName) { + // For Filter/Function/Test calls, consult the PHP callable + // signature: skip wrapping arguments whose param type cannot + // implicitly string-coerce (e.g. `int`, a `final` value object). + if (null !== $params && 'arguments' === $childName) { + $this->wrapArguments($node, $params); + + continue; + } + if (null !== $params && 'node' === $childName && $node instanceof FilterExpression) { + // The filter's input value maps to the first PHP parameter. + if (isset($params[0]) && CallableParameters::isStringCoercionSafe($params[0]->getType(), $params[0]->getDeclaringClass())) { + continue; + } + } + $this->wrapNode($node, $childName); + } + } + + return $node; + } + + public function leaveNode(Node $node, Environment $env): ?Node + { + if ($node instanceof ModuleNode) { + $this->inAModule = false; + + $node->setNode('constructor_end', new Nodes([new CheckSecurityCallNode(), $node->getNode('constructor_end')])); + $node->setNode('class_end', new Nodes([new CheckSecurityNode($this->filters, $this->tags, $this->functions, $this->tests), $node->getNode('class_end')])); + } + + return $node; + } + + /** + * Wraps each entry in the `arguments` slot only when the corresponding + * PHP parameter type can implicitly string-coerce. + * + * @param list<\ReflectionParameter> $params parameters relative to the + * first template argument (for + * filters and tests: starting + * after the `node`/input + * parameter) + */ + private function wrapArguments(Node $node, array $params): void + { + $arguments = $node->getNode('arguments'); + if (!$arguments instanceof Nodes && !$arguments instanceof ArrayExpression) { + $this->wrapNode($node, 'arguments'); + + return; + } + + // Filters and tests pass their input value (`node`) as the first PHP + // param, so their template arguments start at offset 1. + $positional = \array_slice($params, $node->hasNode('node') ? 1 : 0); + $variadic = null; + $byName = []; + foreach ($positional as $p) { + if ($p->isVariadic()) { + $variadic = $p; + break; + } + $byName[$this->normalizeName($p->getName())] ??= $p; + } + + $positionalIdx = 0; + foreach ($arguments as $key => $_) { + if (\is_int($key)) { + $param = $positional[$positionalIdx] ?? $variadic; + if (null !== $param && !$param->isVariadic()) { + ++$positionalIdx; + } + } else { + $param = $byName[$this->normalizeName($key)] ?? $variadic; + } + + if (null !== $param && CallableParameters::isStringCoercionSafe($param->getType(), $param->getDeclaringClass())) { + continue; + } + $this->wrapNode($arguments, (string) $key); + } + } + + private function normalizeName(string $name): string + { + return strtolower(str_replace('_', '', $name)); + } + + private function wrapNode(Node $node, string $name): void + { + $expr = $node->getNode($name); + // `_self` is internal: it compiles to `$this->getTemplateName()` and is always a string + if ($expr instanceof ContextVariable && '_self' === $expr->getAttribute('name')) { + return; + } + if (($expr instanceof ContextVariable || $expr instanceof GetAttrExpression) && !$expr->isGenerator()) { + $node->setNode($name, new CheckToStringNode($expr)); + } elseif ($expr instanceof SpreadUnary) { + $expr->setNode('node', new CheckToStringNode($expr->getNode('node'), true)); + } elseif ($expr instanceof ArrayExpression || $expr instanceof Nodes) { + foreach ($expr as $name => $_) { + $this->wrapNode($expr, $name); + } + } elseif ($expr instanceof OperatorEscapeInterface) { + foreach ($expr->getOperandNamesToEscape() as $operandName) { + $this->wrapNode($expr, $operandName); + } + } elseif ($expr instanceof FilterExpression || $expr instanceof FunctionExpression) { + $node->setNode($name, new CheckToStringNode($expr)); + } + } + + private function isTagAlwaysAllowedInSandbox(Environment $env, string $name): bool + { + if (null === $parser = $env->getTokenParser($name)) { + return false; + } + + return self::isAlwaysAllowedInSandbox($parser); + } + + private function isFilterAlwaysAllowedInSandbox(Environment $env, FilterExpression $node): bool + { + if ($node->hasAttribute('twig_callable')) { + $filter = $node->getAttribute('twig_callable'); + } elseif (null === $filter = $env->getFilter($node->getAttribute('name'))) { + return false; + } + + return self::isAlwaysAllowedInSandbox($filter); + } + + private function isFunctionAlwaysAllowedInSandbox(Environment $env, FunctionExpression $node): bool + { + if ($node->hasAttribute('twig_callable')) { + $function = $node->getAttribute('twig_callable'); + } elseif (null === $function = $env->getFunction($node->getAttribute('name'))) { + return false; + } + + return self::isAlwaysAllowedInSandbox($function); + } + + private function isTestAlwaysAllowedInSandbox(Environment $env, TestExpression $node): bool + { + if ($node->hasAttribute('twig_callable')) { + $test = $node->getAttribute('twig_callable'); + } elseif (null === $test = $env->getTest($node->getAttribute('name'))) { + return false; + } + + return self::isAlwaysAllowedInSandbox($test); + } + + private function isSandboxedFunctionAlwaysAllowedInSandbox(Environment $env, Node $node, string $name): bool + { + if ($node->hasAttribute('sandboxed_function')) { + $function = $node->getAttribute('sandboxed_function'); + } elseif (null === $function = $env->getFunction($name)) { + return false; + } + + return self::isAlwaysAllowedInSandbox($function); + } + + private function isFunctionNameAlwaysAllowedInSandbox(Environment $env, string $name): bool + { + if (null === $function = $env->getFunction($name)) { + return false; + } + + return self::isAlwaysAllowedInSandbox($function); + } + + /** + * @param TwigCallableInterface|TokenParserInterface $subject + */ + private static function isAlwaysAllowedInSandbox($subject): bool + { + if (method_exists($subject, 'isAlwaysAllowedInSandbox')) { + return $subject->isAlwaysAllowedInSandbox(); + } + + $interface = $subject instanceof TokenParserInterface ? TokenParserInterface::class : TwigCallableInterface::class; + trigger_deprecation('twig/twig', '3.28', 'Not implementing the "isAlwaysAllowedInSandbox()" method in "%s" is deprecated. This method will be part of the "%s" interface in 4.0.', $subject::class, $interface); + + return false; + } + + public function getPriority(): int + { + return 0; + } +} diff --git a/novaconium/vendor/twig/src/NodeVisitor/YieldNotReadyNodeVisitor.php b/novaconium/vendor/twig/src/NodeVisitor/YieldNotReadyNodeVisitor.php new file mode 100644 index 0000000..4d6cf60 --- /dev/null +++ b/novaconium/vendor/twig/src/NodeVisitor/YieldNotReadyNodeVisitor.php @@ -0,0 +1,59 @@ +yieldReadyNodes[$class])) { + return $node; + } + + if (!$this->yieldReadyNodes[$class] = (bool) (new \ReflectionClass($class))->getAttributes(YieldReady::class)) { + if ($this->useYield) { + throw new \LogicException(\sprintf('You cannot enable the "use_yield" option of Twig as node "%s" is not marked as ready for it; please make it ready and then flag it with the #[\Twig\Attribute\YieldReady] attribute.', $class)); + } + + trigger_deprecation('twig/twig', '3.9', 'Twig node "%s" is not marked as ready for using "yield" instead of "echo"; please make it ready and then flag it with the #[\Twig\Attribute\YieldReady] attribute.', $class); + } + + return $node; + } + + public function leaveNode(Node $node, Environment $env): ?Node + { + return $node; + } + + public function getPriority(): int + { + return 255; + } +} diff --git a/novaconium/vendor/twig/src/OperatorPrecedenceChange.php b/novaconium/vendor/twig/src/OperatorPrecedenceChange.php new file mode 100644 index 0000000..31ebaef --- /dev/null +++ b/novaconium/vendor/twig/src/OperatorPrecedenceChange.php @@ -0,0 +1,34 @@ + + * + * @deprecated since Twig 1.20 Use Twig\ExpressionParser\PrecedenceChange instead + */ +class OperatorPrecedenceChange extends PrecedenceChange +{ + public function __construct( + private string $package, + private string $version, + private int $newPrecedence, + ) { + trigger_deprecation('twig/twig', '3.21', 'The "%s" class is deprecated since Twig 3.21. Use "%s" instead.', self::class, PrecedenceChange::class); + + parent::__construct($package, $version, $newPrecedence); + } +} diff --git a/novaconium/vendor/twig/src/Parser.php b/novaconium/vendor/twig/src/Parser.php new file mode 100644 index 0000000..5ef1aea --- /dev/null +++ b/novaconium/vendor/twig/src/Parser.php @@ -0,0 +1,612 @@ + + */ +class Parser +{ + private $stack = []; + private ?\WeakMap $expressionRefs = null; + private $stream; + private $parent; + private $visitors; + private $expressionParser; + private $blocks; + private $blockStack; + private $macros; + private $importedSymbols; + private $traits; + private $embeddedTemplates = []; + private int $lastEmbedIndex = 0; + private $varNameSalt = 0; + private $ignoreUnknownTwigCallables = false; + private ExpressionParsers $parsers; + + public function __construct( + private Environment $env, + ) { + $this->parsers = $env->getExpressionParsers(); + } + + public function getEnvironment(): Environment + { + return $this->env; + } + + public function getVarName(): string + { + trigger_deprecation('twig/twig', '3.15', 'The "%s()" method is deprecated.', __METHOD__); + + return \sprintf('__internal_parse_%d', $this->varNameSalt++); + } + + /** + * @throws SyntaxError + */ + public function parse(TokenStream $stream, $test = null, bool $dropNeedle = false): ModuleNode + { + // reset on root parse() calls only, so the counter spans nested/reentrant parses + if (!$this->stack) { + $this->lastEmbedIndex = 0; + } + + $vars = get_object_vars($this); + unset($vars['stack'], $vars['env'], $vars['handlers'], $vars['visitors'], $vars['expressionParser'], $vars['reservedMacroNames'], $vars['lastEmbedIndex'], $vars['varNameSalt']); + $this->stack[] = $vars; + + // node visitors + if (null === $this->visitors) { + $this->visitors = $this->env->getNodeVisitors(); + } + + $this->stream = $stream; + $this->parent = null; + $this->blocks = []; + $this->macros = []; + $this->traits = []; + $this->blockStack = []; + $this->importedSymbols = [[]]; + $this->embeddedTemplates = []; + $this->expressionRefs = new \WeakMap(); + + try { + $body = $this->subparse($test, $dropNeedle); + } catch (SyntaxError $e) { + if (!$e->getSourceContext()) { + $e->setSourceContext($this->stream->getSourceContext()); + } + + if (!$e->getTemplateLine()) { + $e->setTemplateLine($this->getCurrentToken()->getLine()); + } + + throw $e; + } finally { + $this->expressionRefs = null; + } + + if ($this->parent) { + $body = $this->cleanupBodyForChildTemplates($body); + } + + $node = new ModuleNode( + new BodyNode([$body]), + $this->parent, + $this->blocks ? new Nodes($this->blocks) : new EmptyNode(), + $this->macros ? new Nodes($this->macros) : new EmptyNode(), + $this->traits ? new Nodes($this->traits) : new EmptyNode(), + $this->embeddedTemplates ? new Nodes($this->embeddedTemplates) : new EmptyNode(), + $stream->getSourceContext(), + ); + + $traverser = new NodeTraverser($this->env, $this->visitors); + + /** + * @var ModuleNode $node + */ + $node = $traverser->traverse($node); + + // restore previous stack so previous parse() call can resume working + foreach (array_pop($this->stack) as $key => $val) { + $this->$key = $val; + } + + return $node; + } + + public function shouldIgnoreUnknownTwigCallables(): bool + { + return $this->ignoreUnknownTwigCallables; + } + + public function subparseIgnoreUnknownTwigCallables($test, bool $dropNeedle = false): void + { + $previous = $this->ignoreUnknownTwigCallables; + $this->ignoreUnknownTwigCallables = true; + try { + $this->subparse($test, $dropNeedle); + } finally { + $this->ignoreUnknownTwigCallables = $previous; + } + } + + /** + * @throws SyntaxError + */ + public function subparse($test, bool $dropNeedle = false): Node + { + $lineno = $this->getCurrentToken()->getLine(); + $rv = []; + while (!$this->stream->isEOF()) { + switch (true) { + case $this->stream->getCurrent()->test(Token::TEXT_TYPE): + $token = $this->stream->next(); + $rv[] = new TextNode($token->getValue(), $token->getLine()); + break; + + case $this->stream->getCurrent()->test(Token::VAR_START_TYPE): + $token = $this->stream->next(); + $expr = $this->parseExpression(); + $this->stream->expect(Token::VAR_END_TYPE); + $rv[] = new PrintNode($expr, $token->getLine()); + break; + + case $this->stream->getCurrent()->test(Token::BLOCK_START_TYPE): + $this->stream->next(); + $token = $this->getCurrentToken(); + + if (!$token->test(Token::NAME_TYPE)) { + throw new SyntaxError('A block must start with a tag name.', $token->getLine(), $this->stream->getSourceContext()); + } + + if (null !== $test && $test($token)) { + if ($dropNeedle) { + $this->stream->next(); + } + + if (1 === \count($rv)) { + return $rv[0]; + } + + return new Nodes($rv, $lineno); + } + + if (!$subparser = $this->env->getTokenParser($token->getValue())) { + if (null !== $test) { + $e = new SyntaxError(\sprintf('Unexpected "%s" tag', $token->getValue()), $token->getLine(), $this->stream->getSourceContext()); + + $callable = (new ReflectionCallable(new TwigTest('decision', $test)))->getCallable(); + if (\is_array($callable) && $callable[0] instanceof TokenParserInterface) { + $e->appendMessage(\sprintf(' (expecting closing tag for the "%s" tag defined near line %s).', $callable[0]->getTag(), $lineno)); + } + } else { + $e = new SyntaxError(\sprintf('Unknown "%s" tag.', $token->getValue()), $token->getLine(), $this->stream->getSourceContext()); + $e->addSuggestions($token->getValue(), array_keys($this->env->getTokenParsers())); + } + + throw $e; + } + + $this->stream->next(); + + $subparser->setParser($this); + $node = $subparser->parse($token); + if (!$node) { + trigger_deprecation('twig/twig', '3.12', 'Returning "null" from "%s" is deprecated and forbidden by "TokenParserInterface".', $subparser::class); + } else { + $node->setNodeTag($subparser->getTag()); + $rv[] = $node; + } + break; + + default: + throw new SyntaxError('The lexer or the parser ended up in an unsupported state.', $this->getCurrentToken()->getLine(), $this->stream->getSourceContext()); + } + } + + if (1 === \count($rv)) { + return $rv[0]; + } + + return new Nodes($rv, $lineno); + } + + public function getBlockStack(): array + { + trigger_deprecation('twig/twig', '3.12', 'Method "%s()" is deprecated.', __METHOD__); + + return $this->blockStack; + } + + /** + * @return string|null + */ + public function peekBlockStack() + { + return $this->blockStack[\count($this->blockStack) - 1] ?? null; + } + + public function popBlockStack(): void + { + array_pop($this->blockStack); + } + + public function pushBlockStack($name): void + { + $this->blockStack[] = $name; + } + + public function hasBlock(string $name): bool + { + trigger_deprecation('twig/twig', '3.12', 'Method "%s()" is deprecated.', __METHOD__); + + return isset($this->blocks[$name]); + } + + public function getBlock(string $name): Node + { + trigger_deprecation('twig/twig', '3.12', 'Method "%s()" is deprecated.', __METHOD__); + + return $this->blocks[$name]; + } + + public function setBlock(string $name, BlockNode $value): void + { + if (isset($this->blocks[$name])) { + throw new SyntaxError(\sprintf("The block '%s' has already been defined line %d.", $name, $this->blocks[$name]->getTemplateLine()), $this->getCurrentToken()->getLine(), $this->blocks[$name]->getSourceContext()); + } + + $this->blocks[$name] = new BodyNode([$value], [], $value->getTemplateLine()); + } + + public function hasMacro(string $name): bool + { + trigger_deprecation('twig/twig', '3.12', 'Method "%s()" is deprecated.', __METHOD__); + + return isset($this->macros[$name]); + } + + public function setMacro(string $name, MacroNode $node): void + { + $this->macros[$name] = $node; + } + + public function addTrait($trait): void + { + $this->traits[] = $trait; + } + + public function hasTraits(): bool + { + trigger_deprecation('twig/twig', '3.12', 'Method "%s()" is deprecated.', __METHOD__); + + return \count($this->traits) > 0; + } + + /** + * @return void + */ + public function embedTemplate(ModuleNode $template) + { + $template->setIndex(++$this->lastEmbedIndex); + + $this->embeddedTemplates[] = $template; + } + + public function addImportedSymbol(string $type, string $alias, ?string $name = null, AbstractExpression|AssignTemplateVariable|null $internalRef = null): void + { + if ($internalRef && !$internalRef instanceof AssignTemplateVariable) { + trigger_deprecation('twig/twig', '3.15', 'Not passing a "%s" instance as an internal reference is deprecated ("%s" given).', __METHOD__, AssignTemplateVariable::class, $internalRef::class); + + $internalRef = new AssignTemplateVariable(new TemplateVariable($internalRef->getAttribute('name'), $internalRef->getTemplateLine()), $internalRef->getAttribute('global')); + } + + $this->importedSymbols[0][$type][$alias] = ['name' => $name, 'node' => $internalRef]; + } + + /** + * @return array{name: string, node: AssignTemplateVariable|null}|null + */ + public function getImportedSymbol(string $type, string $alias) + { + // if the symbol does not exist in the current scope (0), try in the main/global scope (last index) + return $this->importedSymbols[0][$type][$alias] ?? ($this->importedSymbols[\count($this->importedSymbols) - 1][$type][$alias] ?? null); + } + + public function isMainScope(): bool + { + return 1 === \count($this->importedSymbols); + } + + public function pushLocalScope(): void + { + array_unshift($this->importedSymbols, []); + } + + public function popLocalScope(): void + { + array_shift($this->importedSymbols); + } + + /** + * @deprecated since Twig 3.21 + */ + public function getExpressionParser(): ExpressionParser + { + trigger_deprecation('twig/twig', '3.21', 'Method "%s()" is deprecated, use "parseExpression()" instead.', __METHOD__); + + if (null === $this->expressionParser) { + $this->expressionParser = new ExpressionParser($this, $this->env); + } + + return $this->expressionParser; + } + + public function parseExpression(int $precedence = 0): AbstractExpression + { + $token = $this->getCurrentToken(); + if ($token->test(Token::OPERATOR_TYPE) && $ep = $this->parsers->getByName(PrefixExpressionParserInterface::class, $token->getValue())) { + $this->getStream()->next(); + $expr = $ep->parse($this, $token); + $this->checkPrecedenceDeprecations($ep, $expr); + } else { + $expr = $this->parsers->getByClass(LiteralExpressionParser::class)->parse($this, $token); + } + + $token = $this->getCurrentToken(); + while ($token->test(Token::OPERATOR_TYPE) && ($ep = $this->parsers->getByName(InfixExpressionParserInterface::class, $token->getValue())) && $ep->getPrecedence() >= $precedence) { + $this->getStream()->next(); + $expr = $ep->parse($this, $expr, $token); + $this->checkPrecedenceDeprecations($ep, $expr); + $token = $this->getCurrentToken(); + } + + return $expr; + } + + public function getParent(): ?Node + { + trigger_deprecation('twig/twig', '3.12', 'Method "%s()" is deprecated.', __METHOD__); + + return $this->parent; + } + + /** + * @return bool + */ + public function hasInheritance() + { + return $this->parent || 0 < \count($this->traits); + } + + public function setParent(?Node $parent, bool $throwOnMultiple = true): void + { + if (null === $parent) { + trigger_deprecation('twig/twig', '3.12', 'Passing "null" to "%s()" is deprecated.', __METHOD__); + } + + if (null !== $parent && !$parent instanceof AbstractExpression) { + trigger_deprecation('twig/twig', '3.24', 'Passing a "%s" instance to "%s()" is deprecated, pass an "AbstractExpression" instance instead.', $parent::class, __METHOD__); + } + + if (null !== $this->parent) { + if (!$throwOnMultiple) { + return; + } + + throw new SyntaxError('Multiple extends tags are forbidden.', $parent->getTemplateLine(), $parent->getSourceContext()); + } + + $this->parent = $parent; + } + + public function getStream(): TokenStream + { + return $this->stream; + } + + public function getCurrentToken(): Token + { + return $this->stream->getCurrent(); + } + + public function getFunction(string $name, int $line): TwigFunction + { + try { + $function = $this->env->getFunction($name); + } catch (SyntaxError $e) { + if (!$this->shouldIgnoreUnknownTwigCallables()) { + throw $e; + } + + $function = null; + } + + if (!$function) { + if ($this->shouldIgnoreUnknownTwigCallables()) { + return new TwigFunction($name, static fn () => ''); + } + $e = new SyntaxError(\sprintf('Unknown "%s" function.', $name), $line, $this->stream->getSourceContext()); + $e->addSuggestions($name, array_keys($this->env->getFunctions())); + + throw $e; + } + + if ($function->isDeprecated()) { + $src = $this->stream->getSourceContext(); + $function->triggerDeprecation($src->getPath() ?: $src->getName(), $line); + } + + return $function; + } + + public function getFilter(string $name, int $line): TwigFilter + { + try { + $filter = $this->env->getFilter($name); + } catch (SyntaxError $e) { + if (!$this->shouldIgnoreUnknownTwigCallables()) { + throw $e; + } + + $filter = null; + } + if (!$filter) { + if ($this->shouldIgnoreUnknownTwigCallables()) { + return new TwigFilter($name, static fn () => ''); + } + $e = new SyntaxError(\sprintf('Unknown "%s" filter.', $name), $line, $this->stream->getSourceContext()); + $e->addSuggestions($name, array_keys($this->env->getFilters())); + + throw $e; + } + + if ($filter->isDeprecated()) { + $src = $this->stream->getSourceContext(); + $filter->triggerDeprecation($src->getPath() ?: $src->getName(), $line); + } + + return $filter; + } + + public function getTest(int $line): TwigTest + { + $name = $this->stream->expect(Token::NAME_TYPE)->getValue(); + + if ($this->stream->test(Token::NAME_TYPE)) { + // try 2-words tests + $name = $name.' '.$this->getCurrentToken()->getValue(); + + try { + $test = $this->env->getTest($name); + } catch (SyntaxError $e) { + if (!$this->shouldIgnoreUnknownTwigCallables()) { + throw $e; + } + + $test = null; + } + $this->stream->next(); + } else { + try { + $test = $this->env->getTest($name); + } catch (SyntaxError $e) { + if (!$this->shouldIgnoreUnknownTwigCallables()) { + throw $e; + } + + $test = null; + } + } + + if (!$test) { + if ($this->shouldIgnoreUnknownTwigCallables()) { + return new TwigTest($name, static fn () => ''); + } + $e = new SyntaxError(\sprintf('Unknown "%s" test.', $name), $line, $this->stream->getSourceContext()); + $e->addSuggestions($name, array_keys($this->env->getTests())); + + throw $e; + } + + if ($test->isDeprecated()) { + $src = $this->stream->getSourceContext(); + $test->triggerDeprecation($src->getPath() ?: $src->getName(), $this->stream->getCurrent()->getLine()); + } + + return $test; + } + + private function cleanupBodyForChildTemplates(Node $body): Node + { + if ($body instanceof BlockReferenceNode || ($body instanceof TextNode && $body->isBlank())) { + return new EmptyNode(); + } + + foreach ($body as $k => $node) { + if ($node instanceof BlockReferenceNode) { + // as it has a parent, the block reference won't be used + $body->removeNode($k); + } elseif ($node instanceof TextNode && $node->isBlank()) { + // remove nodes considered as "empty" + $body->removeNode($k); + } + } + + return $body; + } + + private function checkPrecedenceDeprecations(ExpressionParserInterface $expressionParser, AbstractExpression $expr) + { + $this->expressionRefs[$expr] = $expressionParser; + $precedenceChanges = $this->parsers->getPrecedenceChanges(); + + // Check that the all nodes that are between the 2 precedences have explicit parentheses + if (!isset($precedenceChanges[$expressionParser])) { + return; + } + + if ($expr->hasExplicitParentheses()) { + return; + } + + if ($expressionParser instanceof PrefixExpressionParserInterface) { + /** @var AbstractExpression $node */ + $node = $expr->getNode('node'); + foreach ($precedenceChanges as $ep => $changes) { + if (!\in_array($expressionParser, $changes, true)) { + continue; + } + if (isset($this->expressionRefs[$node]) && $ep === $this->expressionRefs[$node]) { + $change = $expressionParser->getPrecedenceChange(); + trigger_deprecation($change->getPackage(), $change->getVersion(), \sprintf('As the "%s" %s operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "%s" at line %d.', $expressionParser->getName(), ExpressionParserType::getType($expressionParser)->value, $this->getStream()->getSourceContext()->getName(), $node->getTemplateLine())); + } + } + } + + foreach ($precedenceChanges[$expressionParser] as $ep) { + foreach ($expr as $node) { + /** @var AbstractExpression $node */ + if (isset($this->expressionRefs[$node]) && $ep === $this->expressionRefs[$node] && !$node->hasExplicitParentheses()) { + $change = $ep->getPrecedenceChange(); + trigger_deprecation($change->getPackage(), $change->getVersion(), \sprintf('As the "%s" %s operator will change its precedence in the next major version, add explicit parentheses to avoid behavior change in "%s" at line %d.', $ep->getName(), ExpressionParserType::getType($ep)->value, $this->getStream()->getSourceContext()->getName(), $node->getTemplateLine())); + } + } + } + } +} diff --git a/novaconium/vendor/twig/src/Profiler/Dumper/BaseDumper.php b/novaconium/vendor/twig/src/Profiler/Dumper/BaseDumper.php new file mode 100644 index 0000000..06bf4b8 --- /dev/null +++ b/novaconium/vendor/twig/src/Profiler/Dumper/BaseDumper.php @@ -0,0 +1,68 @@ + + */ +abstract class BaseDumper +{ + private $root; + + public function dump(Profile $profile): string + { + return $this->dumpProfile($profile); + } + + abstract protected function formatTemplate(Profile $profile, $prefix): string; + + abstract protected function formatNonTemplate(Profile $profile, $prefix): string; + + abstract protected function formatTime(Profile $profile, $percent): string; + + protected function formatRoot(Profile $profile): string + { + return $profile->getName(); + } + + private function dumpProfile(Profile $profile, $prefix = '', $sibling = false): string + { + if ($profile->isRoot()) { + $this->root = $profile->getDuration(); + $start = $this->formatRoot($profile); + } else { + if ($profile->isTemplate()) { + $start = $this->formatTemplate($profile, $prefix); + } else { + $start = $this->formatNonTemplate($profile, $prefix); + } + $prefix .= $sibling ? '│ ' : ' '; + } + + $percent = $this->root ? $profile->getDuration() / $this->root * 100 : 0; + + if ($profile->getDuration() * 1000 < 1) { + $str = $start."\n"; + } else { + $str = \sprintf("%s %s\n", $start, $this->formatTime($profile, $percent)); + } + + $nCount = \count($profile->getProfiles()); + foreach ($profile as $i => $p) { + $str .= $this->dumpProfile($p, $prefix, $i + 1 !== $nCount); + } + + return $str; + } +} diff --git a/novaconium/vendor/twig/src/Profiler/Dumper/BlackfireDumper.php b/novaconium/vendor/twig/src/Profiler/Dumper/BlackfireDumper.php new file mode 100644 index 0000000..7cfae16 --- /dev/null +++ b/novaconium/vendor/twig/src/Profiler/Dumper/BlackfireDumper.php @@ -0,0 +1,72 @@ + + */ +final class BlackfireDumper +{ + public function dump(Profile $profile): string + { + $data = []; + $this->dumpProfile('main()', $profile, $data); + $this->dumpChildren('main()', $profile, $data); + + $start = \sprintf('%f', microtime(true)); + $str = << $values) { + $str .= "$name//{$values['ct']} {$values['wt']} {$values['mu']} {$values['pmu']}\n"; + } + + return $str; + } + + private function dumpChildren(string $parent, Profile $profile, &$data): void + { + foreach ($profile as $p) { + if ($p->isTemplate()) { + $name = $p->getTemplate(); + } else { + $name = \sprintf('%s::%s(%s)', $p->getTemplate(), $p->getType(), $p->getName()); + } + $this->dumpProfile(\sprintf('%s==>%s', $parent, $name), $p, $data); + $this->dumpChildren($name, $p, $data); + } + } + + private function dumpProfile(string $edge, Profile $profile, &$data): void + { + if (isset($data[$edge])) { + ++$data[$edge]['ct']; + $data[$edge]['wt'] += floor($profile->getDuration() * 1000000); + $data[$edge]['mu'] += $profile->getMemoryUsage(); + $data[$edge]['pmu'] += $profile->getPeakMemoryUsage(); + } else { + $data[$edge] = [ + 'ct' => 1, + 'wt' => floor($profile->getDuration() * 1000000), + 'mu' => $profile->getMemoryUsage(), + 'pmu' => $profile->getPeakMemoryUsage(), + ]; + } + } +} diff --git a/novaconium/vendor/twig/src/Profiler/Dumper/HtmlDumper.php b/novaconium/vendor/twig/src/Profiler/Dumper/HtmlDumper.php new file mode 100644 index 0000000..1bfd81b --- /dev/null +++ b/novaconium/vendor/twig/src/Profiler/Dumper/HtmlDumper.php @@ -0,0 +1,57 @@ + + */ +final class HtmlDumper extends BaseDumper +{ + private static $colors = [ + 'block' => '#dfd', + 'macro' => '#ddf', + 'template' => '#ffd', + 'big' => '#d44', + ]; + + public function dump(Profile $profile): string + { + return '
'.parent::dump($profile).'
'; + } + + protected function formatRoot(Profile $profile): string + { + return self::escape($profile->getName()); + } + + protected function formatTemplate(Profile $profile, $prefix): string + { + return \sprintf('%s└ %s', $prefix, self::$colors['template'], self::escape($profile->getTemplate())); + } + + protected function formatNonTemplate(Profile $profile, $prefix): string + { + return \sprintf('%s└ %s::%s(%s)', $prefix, self::escape($profile->getTemplate()), $profile->getType(), self::$colors[$profile->getType()] ?? 'auto', self::escape($profile->getName())); + } + + protected function formatTime(Profile $profile, $percent): string + { + return \sprintf('%.2fms/%.0f%%', $percent > 20 ? self::$colors['big'] : 'auto', $profile->getDuration() * 1000, $percent); + } + + private static function escape(string $value): string + { + return htmlspecialchars($value, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8'); + } +} diff --git a/novaconium/vendor/twig/src/Profiler/Dumper/TextDumper.php b/novaconium/vendor/twig/src/Profiler/Dumper/TextDumper.php new file mode 100644 index 0000000..1c1f77e --- /dev/null +++ b/novaconium/vendor/twig/src/Profiler/Dumper/TextDumper.php @@ -0,0 +1,35 @@ + + */ +final class TextDumper extends BaseDumper +{ + protected function formatTemplate(Profile $profile, $prefix): string + { + return \sprintf('%s└ %s', $prefix, $profile->getTemplate()); + } + + protected function formatNonTemplate(Profile $profile, $prefix): string + { + return \sprintf('%s└ %s::%s(%s)', $prefix, $profile->getTemplate(), $profile->getType(), $profile->getName()); + } + + protected function formatTime(Profile $profile, $percent): string + { + return \sprintf('%.2fms/%.0f%%', $profile->getDuration() * 1000, $percent); + } +} diff --git a/novaconium/vendor/twig/src/Profiler/Node/EnterProfileNode.php b/novaconium/vendor/twig/src/Profiler/Node/EnterProfileNode.php new file mode 100644 index 0000000..4d8e504 --- /dev/null +++ b/novaconium/vendor/twig/src/Profiler/Node/EnterProfileNode.php @@ -0,0 +1,44 @@ + + */ +#[YieldReady] +class EnterProfileNode extends Node +{ + public function __construct(string $extensionName, string $type, string $name, string $varName) + { + parent::__construct([], ['extension_name' => $extensionName, 'name' => $name, 'type' => $type, 'var_name' => $varName]); + } + + public function compile(Compiler $compiler): void + { + $compiler + ->write(\sprintf('$%s = $this->extensions[', $this->getAttribute('var_name'))) + ->repr($this->getAttribute('extension_name')) + ->raw("];\n") + ->write(\sprintf('$%s->enter($%s = new \Twig\Profiler\Profile($this->getTemplateName(), ', $this->getAttribute('var_name'), $this->getAttribute('var_name').'_prof')) + ->repr($this->getAttribute('type')) + ->raw(', ') + ->repr($this->getAttribute('name')) + ->raw("));\n\n") + ; + } +} diff --git a/novaconium/vendor/twig/src/Profiler/Node/LeaveProfileNode.php b/novaconium/vendor/twig/src/Profiler/Node/LeaveProfileNode.php new file mode 100644 index 0000000..bd9227e --- /dev/null +++ b/novaconium/vendor/twig/src/Profiler/Node/LeaveProfileNode.php @@ -0,0 +1,38 @@ + + */ +#[YieldReady] +class LeaveProfileNode extends Node +{ + public function __construct(string $varName) + { + parent::__construct([], ['var_name' => $varName]); + } + + public function compile(Compiler $compiler): void + { + $compiler + ->write("\n") + ->write(\sprintf("\$%s->leave(\$%s);\n\n", $this->getAttribute('var_name'), $this->getAttribute('var_name').'_prof')) + ; + } +} diff --git a/novaconium/vendor/twig/src/Profiler/NodeVisitor/ProfilerNodeVisitor.php b/novaconium/vendor/twig/src/Profiler/NodeVisitor/ProfilerNodeVisitor.php new file mode 100644 index 0000000..4c5c200 --- /dev/null +++ b/novaconium/vendor/twig/src/Profiler/NodeVisitor/ProfilerNodeVisitor.php @@ -0,0 +1,70 @@ + + */ +final class ProfilerNodeVisitor implements NodeVisitorInterface +{ + private $varName; + + public function __construct( + private string $extensionName, + ) { + $this->varName = \sprintf('__internal_%s', hash(\PHP_VERSION_ID < 80100 ? 'sha256' : 'xxh128', $extensionName)); + } + + public function enterNode(Node $node, Environment $env): Node + { + return $node; + } + + public function leaveNode(Node $node, Environment $env): ?Node + { + if ($node instanceof ModuleNode) { + $node->setNode('display_start', new Nodes([new EnterProfileNode($this->extensionName, Profile::TEMPLATE, $node->getTemplateName(), $this->varName), $node->getNode('display_start')])); + $node->setNode('display_end', new Nodes([new LeaveProfileNode($this->varName), $node->getNode('display_end')])); + } elseif ($node instanceof BlockNode) { + $node->setNode('body', new BodyNode([ + new EnterProfileNode($this->extensionName, Profile::BLOCK, $node->getAttribute('name'), $this->varName), + $node->getNode('body'), + new LeaveProfileNode($this->varName), + ])); + } elseif ($node instanceof MacroNode) { + $node->setNode('body', new BodyNode([ + new EnterProfileNode($this->extensionName, Profile::MACRO, $node->getAttribute('name'), $this->varName), + $node->getNode('body'), + new LeaveProfileNode($this->varName), + ])); + } + + return $node; + } + + public function getPriority(): int + { + return 0; + } +} diff --git a/novaconium/vendor/twig/src/Profiler/Profile.php b/novaconium/vendor/twig/src/Profiler/Profile.php new file mode 100644 index 0000000..63861d6 --- /dev/null +++ b/novaconium/vendor/twig/src/Profiler/Profile.php @@ -0,0 +1,194 @@ + + */ +final class Profile implements \IteratorAggregate, \Serializable +{ + public const ROOT = 'ROOT'; + public const BLOCK = 'block'; + public const TEMPLATE = 'template'; + public const MACRO = 'macro'; + private $starts = []; + private $ends = []; + private $profiles = []; + + public function __construct( + private string $template = 'main', + private string $type = self::ROOT, + private string $name = 'main', + ) { + $this->name = str_starts_with($name, '__internal_') ? 'INTERNAL' : $name; + $this->enter(); + } + + public function getTemplate(): string + { + return $this->template; + } + + public function getType(): string + { + return $this->type; + } + + public function getName(): string + { + return $this->name; + } + + public function isRoot(): bool + { + return self::ROOT === $this->type; + } + + public function isTemplate(): bool + { + return self::TEMPLATE === $this->type; + } + + public function isBlock(): bool + { + return self::BLOCK === $this->type; + } + + public function isMacro(): bool + { + return self::MACRO === $this->type; + } + + /** + * @return Profile[] + */ + public function getProfiles(): array + { + return $this->profiles; + } + + public function addProfile(self $profile): void + { + $this->profiles[] = $profile; + } + + /** + * Returns the duration in microseconds. + */ + public function getDuration(): float + { + if ($this->isRoot() && $this->profiles) { + // for the root node with children, duration is the sum of all child durations + $duration = 0; + foreach ($this->profiles as $profile) { + $duration += $profile->getDuration(); + } + + return $duration; + } + + return isset($this->ends['wt']) && isset($this->starts['wt']) ? $this->ends['wt'] - $this->starts['wt'] : 0; + } + + /** + * Returns the start time in microseconds. + */ + public function getStartTime(): float + { + return $this->starts['wt'] ?? 0.0; + } + + /** + * Returns the end time in microseconds. + */ + public function getEndTime(): float + { + return $this->ends['wt'] ?? 0.0; + } + + /** + * Returns the memory usage in bytes. + */ + public function getMemoryUsage(): int + { + return isset($this->ends['mu']) && isset($this->starts['mu']) ? $this->ends['mu'] - $this->starts['mu'] : 0; + } + + /** + * Returns the peak memory usage in bytes. + */ + public function getPeakMemoryUsage(): int + { + return isset($this->ends['pmu']) && isset($this->starts['pmu']) ? $this->ends['pmu'] - $this->starts['pmu'] : 0; + } + + /** + * Starts the profiling. + */ + public function enter(): void + { + $this->starts = [ + 'wt' => microtime(true), + 'mu' => memory_get_usage(), + 'pmu' => memory_get_peak_usage(), + ]; + } + + /** + * Stops the profiling. + */ + public function leave(): void + { + $this->ends = [ + 'wt' => microtime(true), + 'mu' => memory_get_usage(), + 'pmu' => memory_get_peak_usage(), + ]; + } + + public function reset(): void + { + $this->starts = $this->ends = $this->profiles = []; + $this->enter(); + } + + public function getIterator(): \Traversable + { + return new \ArrayIterator($this->profiles); + } + + public function serialize(): string + { + return serialize($this->__serialize()); + } + + public function unserialize($data): void + { + $this->__unserialize(unserialize($data, ['allowed_classes' => [self::class]])); + } + + /** + * @internal + */ + public function __serialize(): array + { + return [$this->template, $this->name, $this->type, $this->starts, $this->ends, $this->profiles]; + } + + /** + * @internal + */ + public function __unserialize(array $data): void + { + [$this->template, $this->name, $this->type, $this->starts, $this->ends, $this->profiles] = $data; + } +} diff --git a/novaconium/vendor/twig/src/Resources/core.php b/novaconium/vendor/twig/src/Resources/core.php new file mode 100644 index 0000000..80f62cf --- /dev/null +++ b/novaconium/vendor/twig/src/Resources/core.php @@ -0,0 +1,572 @@ +getCharset(), $values, $max); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_date_format_filter(Environment $env, $date, $format = null, $timezone = null) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return $env->getExtension(CoreExtension::class)->formatDate($date, $format, $timezone); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_date_modify_filter(Environment $env, $date, $modifier) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return $env->getExtension(CoreExtension::class)->modifyDate($date, $modifier); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_sprintf($format, ...$values) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::sprintf($format, ...$values); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_date_converter(Environment $env, $date = null, $timezone = null) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return $env->getExtension(CoreExtension::class)->convertDate($date, $timezone); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_replace_filter($str, $from) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::replace($str, $from); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_round($value, $precision = 0, $method = 'common') +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::round($value, $precision, $method); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_number_format_filter(Environment $env, $number, $decimal = null, $decimalPoint = null, $thousandSep = null) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return $env->getExtension(CoreExtension::class)->formatNumber($number, $decimal, $decimalPoint, $thousandSep); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_urlencode_filter($url) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::urlencode($url); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_array_merge(...$arrays) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::merge(...$arrays); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_slice(Environment $env, $item, $start, $length = null, $preserveKeys = false) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::slice($env->getCharset(), $item, $start, $length, $preserveKeys); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_first(Environment $env, $item) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::first($env->getCharset(), $item); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_last(Environment $env, $item) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::last($env->getCharset(), $item); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_join_filter($value, $glue = '', $and = null) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::join($value, $glue, $and); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_split_filter(Environment $env, $value, $delimiter, $limit = null) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::split($env->getCharset(), $value, $delimiter, $limit); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_get_array_keys_filter($array) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::keys($array); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_reverse_filter(Environment $env, $item, $preserveKeys = false) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::reverse($env->getCharset(), $item, $preserveKeys); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_sort_filter(Environment $env, $array, $arrow = null) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::sort($env, twig_resolve_is_sandboxed($env), $array, $arrow); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_matches(string $regexp, ?string $str) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::matches($regexp, $str); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_trim_filter($string, $characterMask = null, $side = 'both') +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::trim($string, $characterMask, $side); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_nl2br($string) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::nl2br($string); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_spaceless($content) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::spaceless($content); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_convert_encoding($string, $to, $from) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::convertEncoding($string, $to, $from); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_length_filter(Environment $env, $thing) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::length($env->getCharset(), $thing); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_upper_filter(Environment $env, $string) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::upper($env->getCharset(), $string); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_lower_filter(Environment $env, $string) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::lower($env->getCharset(), $string); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_striptags($string, $allowable_tags = null) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::striptags($string, $allowable_tags); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_title_string_filter(Environment $env, $string) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::titleCase($env->getCharset(), $string); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_capitalize_string_filter(Environment $env, $string) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::capitalize($env->getCharset(), $string); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_test_empty($value) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::testEmpty($value); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_test_iterable($value) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return is_iterable($value); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_include(Environment $env, $context, $template, $variables = [], $withContext = true, $ignoreMissing = false, $sandboxed = false) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::include($env, $context, $template, $variables, $withContext, $ignoreMissing, $sandboxed); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_source(Environment $env, $name, $ignoreMissing = false) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::source($env, $name, $ignoreMissing); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_constant($constant, $object = null) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::constant($constant, $object); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_constant_is_defined($constant, $object = null) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::constant($constant, $object, true); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_array_batch($items, $size, $fill = null, $preserveKeys = true) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::batch($items, $size, $fill, $preserveKeys); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_array_column(Environment $env, $array, $name, $index = null): array +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::column($env, twig_resolve_is_sandboxed($env), $array, $name, $index); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_array_filter(Environment $env, $array, $arrow) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::filter($env, twig_resolve_is_sandboxed($env), $array, $arrow); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_array_map(Environment $env, $array, $arrow) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::map($env, twig_resolve_is_sandboxed($env), $array, $arrow); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_array_reduce(Environment $env, $array, $arrow, $initial = null) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::reduce($env, twig_resolve_is_sandboxed($env), $array, $arrow, $initial); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_array_some(Environment $env, $array, $arrow) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::arraySome($env, $array, $arrow, twig_resolve_is_sandboxed($env)); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_array_every(Environment $env, $array, $arrow) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return CoreExtension::arrayEvery($env, $array, $arrow, twig_resolve_is_sandboxed($env)); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_check_arrow_in_sandbox(Environment $env, $arrow, $thing, $type) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + CoreExtension::checkArrow(twig_resolve_is_sandboxed($env), $arrow, $thing, $type); +} + +/** + * Recovers the calling Template's Source by walking the PHP backtrace. + * + * @internal + */ +function twig_resolve_caller_source(): ?Source +{ + foreach (debug_backtrace(\DEBUG_BACKTRACE_PROVIDE_OBJECT | \DEBUG_BACKTRACE_IGNORE_ARGS) as $trace) { + if (isset($trace['object']) && $trace['object'] instanceof Template) { + return $trace['object']->getSourceContext(); + } + } + + return null; +} + +/** + * @internal + */ +function twig_resolve_is_sandboxed(Environment $env): bool +{ + if (!$env->hasExtension(SandboxExtension::class)) { + return false; + } + + return $env->getExtension(SandboxExtension::class)->isSandboxed(twig_resolve_caller_source()); +} diff --git a/novaconium/vendor/twig/src/Resources/debug.php b/novaconium/vendor/twig/src/Resources/debug.php new file mode 100644 index 0000000..a0392ff --- /dev/null +++ b/novaconium/vendor/twig/src/Resources/debug.php @@ -0,0 +1,25 @@ +getRuntime(EscaperRuntime::class)->escape($string, $strategy, $charset, $autoescape); +} + +/** + * @internal + * + * @deprecated since Twig 3.9 + */ +function twig_escape_filter_is_safe(Node $filterArgs) +{ + trigger_deprecation('twig/twig', '3.9', 'Using the internal "%s" function is deprecated.', __FUNCTION__); + + return EscaperExtension::escapeFilterIsSafe($filterArgs); +} diff --git a/novaconium/vendor/twig/src/Resources/string_loader.php b/novaconium/vendor/twig/src/Resources/string_loader.php new file mode 100644 index 0000000..c499e5e --- /dev/null +++ b/novaconium/vendor/twig/src/Resources/string_loader.php @@ -0,0 +1,26 @@ + */ + private $escapers = []; + + /** @internal */ + public $safeClasses = []; + + /** @internal */ + public $safeLookup = []; + + public function __construct( + private $charset = 'UTF-8', + ) { + } + + /** + * Defines a new escaper to be used via the escape filter. + * + * @param string $strategy The strategy name that should be used as a strategy in the escape call + * @param callable(string $string, string $charset): string $callable A valid PHP callable + * + * @return void + */ + public function setEscaper($strategy, callable $callable) + { + $this->escapers[$strategy] = $callable; + } + + /** + * Gets all defined escapers. + * + * @return array An array of escapers + */ + public function getEscapers() + { + return $this->escapers; + } + + /** + * @param array, string[]> $safeClasses + * + * @return void + */ + public function setSafeClasses(array $safeClasses = []) + { + $this->safeClasses = []; + $this->safeLookup = []; + foreach ($safeClasses as $class => $strategies) { + $this->addSafeClass($class, $strategies); + } + } + + /** + * @param class-string<\Stringable> $class + * @param string[] $strategies + * + * @return void + */ + public function addSafeClass(string $class, array $strategies) + { + $class = ltrim($class, '\\'); + if (!isset($this->safeClasses[$class])) { + $this->safeClasses[$class] = []; + } + $this->safeClasses[$class] = array_merge($this->safeClasses[$class], $strategies); + + foreach ($strategies as $strategy) { + $this->safeLookup[$strategy][$class] = true; + } + } + + /** + * Escapes a string. + * + * @param mixed $string The value to be escaped + * @param string $strategy The escaping strategy + * @param string|null $charset The charset + * @param bool $autoescape Whether the function is called by the auto-escaping feature (true) or by the developer (false) + * + * @throws RuntimeError + */ + public function escape($string, string $strategy = 'html', ?string $charset = null, bool $autoescape = false) + { + if ($autoescape && $string instanceof Markup) { + return $string; + } + + if (!\is_string($string)) { + if ($string instanceof \Stringable) { + if ($autoescape) { + $c = $string::class; + if (!isset($this->safeClasses[$c])) { + $this->safeClasses[$c] = []; + foreach (class_parents($string) + class_implements($string) as $class) { + if (isset($this->safeClasses[$class])) { + $this->safeClasses[$c] = array_unique(array_merge($this->safeClasses[$c], $this->safeClasses[$class])); + foreach ($this->safeClasses[$class] as $s) { + $this->safeLookup[$s][$c] = true; + } + } + } + } + if (isset($this->safeLookup[$strategy][$c]) || isset($this->safeLookup['all'][$c])) { + return (string) $string; + } + } + + $string = (string) $string; + } elseif (\in_array($strategy, ['html', 'js', 'css', 'html_attr', 'html_attr_relaxed', 'url'], true)) { + // we return the input as is (which can be of any type) + return $string; + } + } + + if ('' === $string) { + return ''; + } + + $charset = $charset ?: $this->charset; + + switch ($strategy) { + case 'html': + // see https://www.php.net/htmlspecialchars + + if ('UTF-8' === $charset) { + return htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8'); + } + + // Using a static variable to avoid initializing the array + // each time the function is called. Moving the declaration on the + // top of the function slow downs other escaping strategies. + static $htmlspecialcharsCharsets = [ + 'ISO-8859-1' => true, 'ISO8859-1' => true, + 'ISO-8859-15' => true, 'ISO8859-15' => true, + 'utf-8' => true, 'UTF-8' => true, + 'CP866' => true, 'IBM866' => true, '866' => true, + 'CP1251' => true, 'WINDOWS-1251' => true, 'WIN-1251' => true, + '1251' => true, + 'CP1252' => true, 'WINDOWS-1252' => true, '1252' => true, + 'KOI8-R' => true, 'KOI8-RU' => true, 'KOI8R' => true, + 'BIG5' => true, '950' => true, + 'GB2312' => true, '936' => true, + 'BIG5-HKSCS' => true, + 'SHIFT_JIS' => true, 'SJIS' => true, '932' => true, + 'EUC-JP' => true, 'EUCJP' => true, + 'ISO8859-5' => true, 'ISO-8859-5' => true, 'MACROMAN' => true, + ]; + + if (isset($htmlspecialcharsCharsets[$charset])) { + return htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, $charset); + } + + if (isset($htmlspecialcharsCharsets[strtoupper($charset)])) { + // cache the lowercase variant for future iterations + $htmlspecialcharsCharsets[$charset] = true; + + return htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, $charset); + } + + $string = $this->convertEncoding($string, 'UTF-8', $charset); + $string = htmlspecialchars($string, \ENT_QUOTES | \ENT_SUBSTITUTE, 'UTF-8'); + + return iconv('UTF-8', $charset, $string); + + case 'js': + // escape all non-alphanumeric characters + // into their \x or \uHHHH representations + if ('UTF-8' !== $charset) { + $string = $this->convertEncoding($string, 'UTF-8', $charset); + } + + if (!preg_match('//u', $string)) { + throw new RuntimeError('The string to escape is not a valid UTF-8 string.'); + } + + $string = preg_replace_callback('#[^a-zA-Z0-9,\._]#Su', static function ($matches) { + $char = $matches[0]; + + /* + * A few characters have short escape sequences in JSON and JavaScript. + * Escape sequences supported only by JavaScript, not JSON, are omitted. + * \" is also supported but omitted, because the resulting string is not HTML safe. + */ + $short = match ($char) { + '\\' => '\\\\', + '/' => '\\/', + "\x08" => '\b', + "\x0C" => '\f', + "\x0A" => '\n', + "\x0D" => '\r', + "\x09" => '\t', + default => false, + }; + + if ($short) { + return $short; + } + + $codepoint = mb_ord($char, 'UTF-8'); + if (0x10000 > $codepoint) { + return \sprintf('\u%04X', $codepoint); + } + + // Split characters outside the BMP into surrogate pairs + // https://tools.ietf.org/html/rfc2781.html#section-2.1 + $u = $codepoint - 0x10000; + $high = 0xD800 | ($u >> 10); + $low = 0xDC00 | ($u & 0x3FF); + + return \sprintf('\u%04X\u%04X', $high, $low); + }, $string); + + if ('UTF-8' !== $charset) { + $string = iconv('UTF-8', $charset, $string); + } + + return $string; + + case 'css': + if ('UTF-8' !== $charset) { + $string = $this->convertEncoding($string, 'UTF-8', $charset); + } + + if (!preg_match('//u', $string)) { + throw new RuntimeError('The string to escape is not a valid UTF-8 string.'); + } + + $string = preg_replace_callback('#[^a-zA-Z0-9]#Su', static function ($matches) { + $char = $matches[0]; + + return \sprintf('\\%X ', 1 === \strlen($char) ? \ord($char) : mb_ord($char, 'UTF-8')); + }, $string); + + if ('UTF-8' !== $charset) { + $string = iconv('UTF-8', $charset, $string); + } + + return $string; + + case 'html_attr': + case 'html_attr_relaxed': + if ('UTF-8' !== $charset) { + $string = $this->convertEncoding($string, 'UTF-8', $charset); + } + + if (!preg_match('//u', $string)) { + throw new RuntimeError('The string to escape is not a valid UTF-8 string.'); + } + + $regex = match ($strategy) { + 'html_attr' => '#[^a-zA-Z0-9,\.\-_]#Su', + 'html_attr_relaxed' => '#[^a-zA-Z0-9,\.\-_:@\[\]]#Su', + }; + + $string = preg_replace_callback($regex, static function ($matches) { + /** + * This function is adapted from code coming from Zend Framework. + * + * @copyright Copyright (c) 2005-2012 Zend Technologies USA Inc. (https://www.zend.com) + * @license https://framework.zend.com/license/new-bsd New BSD License + */ + $chr = $matches[0]; + $ord = \ord($chr[0]); + + /* + * The following replaces characters undefined in HTML with the + * hex entity for the Unicode replacement character. + */ + if (($ord <= 0x1F && "\t" != $chr && "\n" != $chr && "\r" != $chr) || ($ord >= 0x7F && $ord <= 0x9F)) { + return '�'; + } + + /* + * Check if the current character to escape has a name entity we should + * replace it with while grabbing the hex value of the character. + */ + if (1 === \strlen($chr)) { + /* + * While HTML supports far more named entities, the lowest common denominator + * has become HTML5's XML Serialisation which is restricted to the those named + * entities that XML supports. Using HTML entities would result in this error: + * XML Parsing Error: undefined entity + */ + return match ($ord) { + 34 => '"', /* quotation mark */ + 38 => '&', /* ampersand */ + 60 => '<', /* less-than sign */ + 62 => '>', /* greater-than sign */ + default => \sprintf('&#x%02X;', $ord), + }; + } + + /* + * Per OWASP recommendations, we'll use hex entities for any other + * characters where a named entity does not exist. + */ + return \sprintf('&#x%04X;', mb_ord($chr, 'UTF-8')); + }, $string); + + if ('UTF-8' !== $charset) { + $string = iconv('UTF-8', $charset, $string); + } + + return $string; + + case 'url': + return rawurlencode($string); + + default: + if (\array_key_exists($strategy, $this->escapers)) { + return $this->escapers[$strategy]($string, $charset); + } + + $validStrategies = implode('", "', array_merge(['html', 'js', 'url', 'css', 'html_attr', 'html_attr_relaxed'], array_keys($this->escapers))); + + throw new RuntimeError(\sprintf('Invalid escaping strategy "%s" (valid ones: "%s").', $strategy, $validStrategies)); + } + } + + private function convertEncoding(string $string, string $to, string $from) + { + if (!\function_exists('iconv')) { + throw new RuntimeError('Unable to convert encoding: required function iconv() does not exist. You should install ext-iconv or symfony/polyfill-iconv.'); + } + + return iconv($from, $to, $string); + } +} diff --git a/novaconium/vendor/twig/src/RuntimeLoader/ContainerRuntimeLoader.php b/novaconium/vendor/twig/src/RuntimeLoader/ContainerRuntimeLoader.php new file mode 100644 index 0000000..0510668 --- /dev/null +++ b/novaconium/vendor/twig/src/RuntimeLoader/ContainerRuntimeLoader.php @@ -0,0 +1,35 @@ + + * @author Robin Chalas + */ +class ContainerRuntimeLoader implements RuntimeLoaderInterface +{ + public function __construct( + private ContainerInterface $container, + ) { + } + + public function load(string $class) + { + return $this->container->has($class) ? $this->container->get($class) : null; + } +} diff --git a/novaconium/vendor/twig/src/RuntimeLoader/FactoryRuntimeLoader.php b/novaconium/vendor/twig/src/RuntimeLoader/FactoryRuntimeLoader.php new file mode 100644 index 0000000..5d4e70b --- /dev/null +++ b/novaconium/vendor/twig/src/RuntimeLoader/FactoryRuntimeLoader.php @@ -0,0 +1,39 @@ + + */ +class FactoryRuntimeLoader implements RuntimeLoaderInterface +{ + /** + * @param array $map An array where keys are class names and values factory callables + */ + public function __construct( + private array $map = [], + ) { + } + + public function load(string $class) + { + if (!isset($this->map[$class])) { + return null; + } + + $runtimeFactory = $this->map[$class]; + + return $runtimeFactory(); + } +} diff --git a/novaconium/vendor/twig/src/RuntimeLoader/RuntimeLoaderInterface.php b/novaconium/vendor/twig/src/RuntimeLoader/RuntimeLoaderInterface.php new file mode 100644 index 0000000..9e5b204 --- /dev/null +++ b/novaconium/vendor/twig/src/RuntimeLoader/RuntimeLoaderInterface.php @@ -0,0 +1,27 @@ + + */ +interface RuntimeLoaderInterface +{ + /** + * Creates the runtime implementation of a Twig element (filter/function/test). + * + * @return object|null The runtime instance or null if the loader does not know how to create the runtime for this class + */ + public function load(string $class); +} diff --git a/novaconium/vendor/twig/src/Sandbox/SecurityError.php b/novaconium/vendor/twig/src/Sandbox/SecurityError.php new file mode 100644 index 0000000..30a404f --- /dev/null +++ b/novaconium/vendor/twig/src/Sandbox/SecurityError.php @@ -0,0 +1,23 @@ + + */ +class SecurityError extends Error +{ +} diff --git a/novaconium/vendor/twig/src/Sandbox/SecurityNotAllowedFilterError.php b/novaconium/vendor/twig/src/Sandbox/SecurityNotAllowedFilterError.php new file mode 100644 index 0000000..9293a3f --- /dev/null +++ b/novaconium/vendor/twig/src/Sandbox/SecurityNotAllowedFilterError.php @@ -0,0 +1,33 @@ + + */ +final class SecurityNotAllowedFilterError extends SecurityError +{ + private string $filterName; + + public function __construct(string $message, string $functionName) + { + parent::__construct($message); + $this->filterName = $functionName; + } + + public function getFilterName(): string + { + return $this->filterName; + } +} diff --git a/novaconium/vendor/twig/src/Sandbox/SecurityNotAllowedFunctionError.php b/novaconium/vendor/twig/src/Sandbox/SecurityNotAllowedFunctionError.php new file mode 100644 index 0000000..71c9f02 --- /dev/null +++ b/novaconium/vendor/twig/src/Sandbox/SecurityNotAllowedFunctionError.php @@ -0,0 +1,33 @@ + + */ +final class SecurityNotAllowedFunctionError extends SecurityError +{ + private string $functionName; + + public function __construct(string $message, string $functionName) + { + parent::__construct($message); + $this->functionName = $functionName; + } + + public function getFunctionName(): string + { + return $this->functionName; + } +} diff --git a/novaconium/vendor/twig/src/Sandbox/SecurityNotAllowedMethodError.php b/novaconium/vendor/twig/src/Sandbox/SecurityNotAllowedMethodError.php new file mode 100644 index 0000000..98e8e43 --- /dev/null +++ b/novaconium/vendor/twig/src/Sandbox/SecurityNotAllowedMethodError.php @@ -0,0 +1,40 @@ + + */ +final class SecurityNotAllowedMethodError extends SecurityError +{ + private string $className; + private string $methodName; + + public function __construct(string $message, string $className, string $methodName) + { + parent::__construct($message); + $this->className = $className; + $this->methodName = $methodName; + } + + public function getClassName(): string + { + return $this->className; + } + + public function getMethodName(): string + { + return $this->methodName; + } +} diff --git a/novaconium/vendor/twig/src/Sandbox/SecurityNotAllowedPropertyError.php b/novaconium/vendor/twig/src/Sandbox/SecurityNotAllowedPropertyError.php new file mode 100644 index 0000000..e74ffed --- /dev/null +++ b/novaconium/vendor/twig/src/Sandbox/SecurityNotAllowedPropertyError.php @@ -0,0 +1,40 @@ + + */ +final class SecurityNotAllowedPropertyError extends SecurityError +{ + private string $className; + private string $propertyName; + + public function __construct(string $message, string $className, string $propertyName) + { + parent::__construct($message); + $this->className = $className; + $this->propertyName = $propertyName; + } + + public function getClassName(): string + { + return $this->className; + } + + public function getPropertyName(): string + { + return $this->propertyName; + } +} diff --git a/novaconium/vendor/twig/src/Sandbox/SecurityNotAllowedTagError.php b/novaconium/vendor/twig/src/Sandbox/SecurityNotAllowedTagError.php new file mode 100644 index 0000000..f9cd625 --- /dev/null +++ b/novaconium/vendor/twig/src/Sandbox/SecurityNotAllowedTagError.php @@ -0,0 +1,33 @@ + + */ +final class SecurityNotAllowedTagError extends SecurityError +{ + private string $tagName; + + public function __construct(string $message, string $tagName) + { + parent::__construct($message); + $this->tagName = $tagName; + } + + public function getTagName(): string + { + return $this->tagName; + } +} diff --git a/novaconium/vendor/twig/src/Sandbox/SecurityNotAllowedTestError.php b/novaconium/vendor/twig/src/Sandbox/SecurityNotAllowedTestError.php new file mode 100644 index 0000000..af702ba --- /dev/null +++ b/novaconium/vendor/twig/src/Sandbox/SecurityNotAllowedTestError.php @@ -0,0 +1,33 @@ + + */ +final class SecurityNotAllowedTestError extends SecurityError +{ + private string $testName; + + public function __construct(string $message, string $testName) + { + parent::__construct($message); + $this->testName = $testName; + } + + public function getTestName(): string + { + return $this->testName; + } +} diff --git a/novaconium/vendor/twig/src/Sandbox/SecurityPolicy.php b/novaconium/vendor/twig/src/Sandbox/SecurityPolicy.php new file mode 100644 index 0000000..7d1172b --- /dev/null +++ b/novaconium/vendor/twig/src/Sandbox/SecurityPolicy.php @@ -0,0 +1,175 @@ + + */ +final class SecurityPolicy implements SecurityPolicyInterface +{ + private $allowedTags; + private $allowedFilters; + private $allowedMethods; + private $allowedProperties; + private $allowedFunctions; + private array $allowedTests; + private bool $strict = false; + + public function __construct(array $allowedTags = [], array $allowedFilters = [], array $allowedMethods = [], array $allowedProperties = [], array $allowedFunctions = [], array $allowedTests = []) + { + $this->allowedTags = $allowedTags; + $this->allowedFilters = $allowedFilters; + $this->setAllowedMethods($allowedMethods); + $this->allowedProperties = $allowedProperties; + $this->allowedFunctions = $allowedFunctions; + $this->allowedTests = $allowedTests; + } + + public function setAllowedTags(array $tags): void + { + $this->allowedTags = $tags; + } + + public function setAllowedFilters(array $filters): void + { + $this->allowedFilters = $filters; + } + + public function setAllowedMethods(array $methods): void + { + $this->allowedMethods = []; + foreach ($methods as $class => $m) { + $this->allowedMethods[$class] = array_map('strtolower', \is_array($m) ? $m : [$m]); + } + } + + public function setAllowedProperties(array $properties): void + { + $this->allowedProperties = $properties; + } + + public function setAllowedFunctions(array $functions): void + { + $this->allowedFunctions = $functions; + } + + public function setAllowedTests(array $tests): void + { + $this->allowedTests = $tests; + } + + /** + * Toggles strict mode. + * + * In strict mode, the tags, functions, and tests that are historically always + * allowed in a sandbox (the ``extends`` and ``use`` tags, the ``parent``, + * ``block``, and ``attribute`` functions, and any test) are no longer implicitly + * allowed and must be added to the relevant allow-list to be usable. Use this + * flag in 3.x to opt-in to the forthcoming 4.0 behavior and silence the related + * deprecations. + */ + public function setStrict(bool $strict): void + { + $this->strict = $strict; + } + + public function checkSecurity($tags, $filters, $functions, array $tests = []): void + { + if (\func_num_args() < 4) { + trigger_deprecation('twig/twig', '3.28', 'Not passing the "$tests" argument to "%s::checkSecurity()" is deprecated; it will be required in 4.0.', static::class); + } + + foreach ($tags as $tag) { + if (!\in_array($tag, $this->allowedTags, true)) { + if (!$this->strict && 'extends' === $tag) { + trigger_deprecation('twig/twig', '3.12', 'The "extends" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed (or enable strict mode on the security policy to opt-in to the 4.0 behavior now).'); + } elseif (!$this->strict && 'use' === $tag) { + trigger_deprecation('twig/twig', '3.12', 'The "use" tag is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed (or enable strict mode on the security policy to opt-in to the 4.0 behavior now).'); + } else { + throw new SecurityNotAllowedTagError(\sprintf('Tag "%s" is not allowed.', $tag), $tag); + } + } + } + + foreach ($filters as $filter) { + if (!\in_array($filter, $this->allowedFilters, true)) { + throw new SecurityNotAllowedFilterError(\sprintf('Filter "%s" is not allowed.', $filter), $filter); + } + } + + foreach ($functions as $function) { + if (!\in_array($function, $this->allowedFunctions, true)) { + if (!$this->strict && 'parent' === $function) { + trigger_deprecation('twig/twig', '3.27', 'The "parent" function is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed (or enable strict mode on the security policy to opt-in to the 4.0 behavior now).'); + } elseif (!$this->strict && 'block' === $function) { + trigger_deprecation('twig/twig', '3.27', 'The "block" function is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed (or enable strict mode on the security policy to opt-in to the 4.0 behavior now).'); + } elseif (!$this->strict && 'attribute' === $function) { + trigger_deprecation('twig/twig', '3.27', 'The "attribute" function is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed (or enable strict mode on the security policy to opt-in to the 4.0 behavior now).'); + } else { + throw new SecurityNotAllowedFunctionError(\sprintf('Function "%s" is not allowed.', $function), $function); + } + } + } + + foreach ($tests as $test) { + if (!\in_array($test, $this->allowedTests, true)) { + if (!$this->strict) { + trigger_deprecation('twig/twig', '3.28', 'The "%s" test is always allowed in sandboxes, but won\'t be in 4.0, please enable it explicitly in your sandbox policy if needed (or enable strict mode on the security policy to opt-in to the 4.0 behavior now).', $test); + } else { + throw new SecurityNotAllowedTestError(\sprintf('Test "%s" is not allowed.', $test), $test); + } + } + } + } + + public function checkMethodAllowed($obj, $method): void + { + if ($obj instanceof Template || $obj instanceof Markup) { + return; + } + + $allowed = false; + $method = strtolower($method); + foreach ($this->allowedMethods as $class => $methods) { + if ($obj instanceof $class && \in_array($method, $methods, true)) { + $allowed = true; + break; + } + } + + if (!$allowed) { + $class = $obj::class; + throw new SecurityNotAllowedMethodError(\sprintf('Calling "%s" method on a "%s" object is not allowed.', $method, $class), $class, $method); + } + } + + public function checkPropertyAllowed($obj, $property): void + { + $allowed = false; + foreach ($this->allowedProperties as $class => $properties) { + if ($obj instanceof $class && \in_array($property, \is_array($properties) ? $properties : [$properties], true)) { + $allowed = true; + break; + } + } + + if (!$allowed) { + $class = $obj::class; + throw new SecurityNotAllowedPropertyError(\sprintf('Calling "%s" property on a "%s" object is not allowed.', $property, $class), $class, $property); + } + } +} diff --git a/novaconium/vendor/twig/src/Sandbox/SecurityPolicyInterface.php b/novaconium/vendor/twig/src/Sandbox/SecurityPolicyInterface.php new file mode 100644 index 0000000..cb36ba5 --- /dev/null +++ b/novaconium/vendor/twig/src/Sandbox/SecurityPolicyInterface.php @@ -0,0 +1,46 @@ + + */ +interface SecurityPolicyInterface +{ + /** + * @param string[] $tags + * @param string[] $filters + * @param string[] $functions + * @param string[] $tests + * + * @throws SecurityError + */ + public function checkSecurity($tags, $filters, $functions/* , array $tests */): void; + + /** + * @param object $obj + * @param string $method + * + * @throws SecurityNotAllowedMethodError + */ + public function checkMethodAllowed($obj, $method): void; + + /** + * @param object $obj + * @param string $property + * + * @throws SecurityNotAllowedPropertyError + */ + public function checkPropertyAllowed($obj, $property): void; +} diff --git a/novaconium/vendor/twig/src/Sandbox/SourcePolicyInterface.php b/novaconium/vendor/twig/src/Sandbox/SourcePolicyInterface.php new file mode 100644 index 0000000..1b29608 --- /dev/null +++ b/novaconium/vendor/twig/src/Sandbox/SourcePolicyInterface.php @@ -0,0 +1,26 @@ + + */ +final class Source +{ + /** + * @param string $code The template source code + * @param string $name The template logical name + * @param string $path The filesystem path of the template if any + */ + public function __construct( + private string $code, + private string $name, + private string $path = '', + ) { + } + + public function getCode(): string + { + return $this->code; + } + + public function getName(): string + { + return $this->name; + } + + public function getPath(): string + { + return $this->path; + } + + /** + * Returns the 1-based column for a 0-based byte offset in the source code. + * + * A negative offset means the position is unknown and yields null. + * + * @return positive-int|null + */ + public function getColumn(int $offset): ?int + { + if ($offset < 0) { + return null; + } + + $before = str_replace(["\r\n", "\r"], "\n", substr($this->code, 0, $offset)); + $lineStart = strrpos($before, "\n"); + + return false === $lineStart ? \strlen($before) + 1 : \strlen($before) - $lineStart; + } +} diff --git a/novaconium/vendor/twig/src/Template.php b/novaconium/vendor/twig/src/Template.php new file mode 100644 index 0000000..3d74460 --- /dev/null +++ b/novaconium/vendor/twig/src/Template.php @@ -0,0 +1,560 @@ +load() + * instead, which returns an instance of \Twig\TemplateWrapper. + * + * @author Fabien Potencier + * + * @internal + */ +abstract class Template +{ + public const ANY_CALL = 'any'; + public const ARRAY_CALL = 'array'; + public const METHOD_CALL = 'method'; + + protected $parent; + protected $parents = []; + protected $blocks = []; + protected $traits = []; + protected $traitAliases = []; + protected $extensions = []; + protected $sandbox; + + private $useYield; + + public function __construct( + protected Environment $env, + ) { + $this->useYield = $env->useYield(); + $this->extensions = $env->getExtensions(); + } + + /** + * Returns the template name. + */ + abstract public function getTemplateName(): string; + + /** + * Returns debug information about the template. + * + * @return array Debug information + */ + abstract public function getDebugInfo(): array; + + /** + * Returns information about the original template source code. + */ + abstract public function getSourceContext(): Source; + + /** + * Returns the parent template. + * + * This method is for internal use only and should never be called + * directly. + * + * @return self|TemplateWrapper|false The parent template or false if there is no parent + */ + public function getParent(array $context): self|TemplateWrapper|false + { + if (null !== $this->parent) { + return $this->parent; + } + + // The compiled doGetParent() may evaluate user expressions (filters, + // functions, method calls) when the parent name is dynamic. Make sure + // the sandbox security check runs first so those expressions cannot + // bypass the allow-list when getParent() is reached before the first + // ensureSecurityChecked() call on this template (e.g. via + // getTemplateForMacro() or yieldBlock() into a pre-warmed instance). + $this->ensureSecurityChecked(); + + if (!$parent = $this->doGetParent($context)) { + return false; + } + + if ($parent instanceof self || $parent instanceof TemplateWrapper) { + return $this->parents[$parent->getSourceContext()->getName()] = $parent; + } + + if (!isset($this->parents[$parent])) { + $this->parents[$parent] = $this->load($parent, -1); + } + + return $this->parents[$parent]; + } + + protected function doGetParent(array $context): bool|string|self|TemplateWrapper + { + return false; + } + + public function isTraitable(): bool + { + return true; + } + + /** + * Displays a parent block. + * + * This method is for internal use only and should never be called + * directly. + * + * @param string $name The block name to display from the parent + * @param array $context The context + * @param array $blocks The current set of blocks + */ + public function displayParentBlock($name, array $context, array $blocks = []): void + { + foreach ($this->yieldParentBlock($name, $context, $blocks) as $data) { + echo $data; + } + } + + /** + * Displays a block. + * + * This method is for internal use only and should never be called + * directly. + * + * @param string $name The block name to display + * @param array $context The context + * @param array $blocks The current set of blocks + * @param bool $useBlocks Whether to use the current set of blocks + */ + public function displayBlock($name, array $context, array $blocks = [], $useBlocks = true, ?self $templateContext = null): void + { + foreach ($this->yieldBlock($name, $context, $blocks, $useBlocks, $templateContext) as $data) { + echo $data; + } + } + + /** + * Renders a parent block. + * + * This method is for internal use only and should never be called + * directly. + * + * @param string $name The block name to render from the parent + * @param array $context The context + * @param array $blocks The current set of blocks + * + * @return string The rendered block + */ + public function renderParentBlock($name, array $context, array $blocks = []): string + { + if (!$this->useYield) { + if ($this->env->isDebug()) { + ob_start(); + } else { + ob_start(static function () { return ''; }); + } + $this->displayParentBlock($name, $context, $blocks); + + return ob_get_clean(); + } + + $content = ''; + foreach ($this->yieldParentBlock($name, $context, $blocks) as $data) { + $content .= $data; + } + + return $content; + } + + /** + * Renders a block. + * + * This method is for internal use only and should never be called + * directly. + * + * @param string $name The block name to render + * @param array $context The context + * @param array $blocks The current set of blocks + * @param bool $useBlocks Whether to use the current set of blocks + * + * @return string The rendered block + */ + public function renderBlock($name, array $context, array $blocks = [], $useBlocks = true): string + { + if (!$this->useYield) { + $level = ob_get_level(); + if ($this->env->isDebug()) { + ob_start(); + } else { + ob_start(static function () { return ''; }); + } + try { + $this->displayBlock($name, $context, $blocks, $useBlocks); + } catch (\Throwable $e) { + while (ob_get_level() > $level) { + ob_end_clean(); + } + + throw $e; + } + + return ob_get_clean(); + } + + $content = ''; + foreach ($this->yieldBlock($name, $context, $blocks, $useBlocks) as $data) { + $content .= $data; + } + + return $content; + } + + /** + * Returns whether a block exists or not in the current context of the template. + * + * This method checks blocks defined in the current template + * or defined in "used" traits or defined in parent templates. + * + * @param string $name The block name + * @param array $context The context + * @param array $blocks The current set of blocks + * + * @return bool true if the block exists, false otherwise + */ + public function hasBlock($name, array $context, array $blocks = []): bool + { + if (isset($blocks[$name])) { + return $blocks[$name][0] instanceof self; + } + + if (isset($this->blocks[$name])) { + return true; + } + + if ($parent = $this->getParent($context)) { + return $parent->hasBlock($name, $context); + } + + return false; + } + + /** + * Returns all block names in the current context of the template. + * + * This method checks blocks defined in the current template + * or defined in "used" traits or defined in parent templates. + * + * @param array $context The context + * @param array $blocks The current set of blocks + * + * @return array An array of block names + */ + public function getBlockNames(array $context, array $blocks = []): array + { + $names = array_merge(array_keys($blocks), array_keys($this->blocks)); + + if ($parent = $this->getParent($context)) { + $names = array_merge($names, $parent->getBlockNames($context)); + } + + return array_unique($names); + } + + /** + * @param string|TemplateWrapper|array $template + */ + protected function load(string|TemplateWrapper|array $template, int $line, ?int $index = null): self + { + try { + if (\is_array($template)) { + return $this->env->resolveTemplate($template)->unwrap(); + } + + if ($template instanceof TemplateWrapper) { + return $template->unwrap(); + } + + if ($template === $this->getTemplateName()) { + $class = static::class; + if (false !== $pos = strrpos($class, '___', -1)) { + $class = substr($class, 0, $pos); + } + } else { + $class = $this->env->getTemplateClass($template); + } + + return $this->env->loadTemplate($class, $template, $index); + } catch (Error $e) { + if (!$e->getSourceContext()) { + $e->setSourceContext($this->getSourceContext()); + } + + if ($e->getTemplateLine() > 0) { + throw $e; + } + + if (-1 === $line) { + $e->guess(); + } else { + $e->setTemplateLine($line); + } + + throw $e; + } + } + + /** + * @param string|TemplateWrapper|array $template + * + * @deprecated since Twig 3.21 and will be removed in 4.0. Use Template::load() instead. + */ + protected function loadTemplate($template, $templateName = null, ?int $line = null, ?int $index = null): self|TemplateWrapper + { + trigger_deprecation('twig/twig', '3.21', 'The "%s" method is deprecated.', __METHOD__); + + if (null === $line) { + $line = -1; + } + + if ($template instanceof self) { + return $template; + } + + return $this->load($template, $line, $index); + } + + /** + * @internal + * + * @return $this + */ + public function unwrap(): self + { + return $this; + } + + /** + * Returns all blocks. + * + * This method is for internal use only and should never be called + * directly. + * + * @return array An array of blocks + */ + public function getBlocks(): array + { + return $this->blocks; + } + + public function display(array $context, array $blocks = []): void + { + foreach ($this->yield($context, $blocks) as $data) { + echo $data; + } + } + + public function render(array $context): string + { + if (!$this->useYield) { + $level = ob_get_level(); + if ($this->env->isDebug()) { + ob_start(); + } else { + ob_start(static function () { return ''; }); + } + try { + $this->display($context); + } catch (\Throwable $e) { + while (ob_get_level() > $level) { + ob_end_clean(); + } + + throw $e; + } + + return ob_get_clean(); + } + + $content = ''; + foreach ($this->yield($context) as $data) { + $content .= $data; + } + + return $content; + } + + /** + * @return iterable + */ + public function yield(array $context, array $blocks = []): iterable + { + $context += $this->env->getGlobals(); + $blocks = array_merge($this->blocks, $blocks); + + try { + $this->ensureSecurityChecked(); + yield from $this->doDisplay($context, $blocks); + } catch (Error $e) { + if (!$e->getSourceContext()) { + $e->setSourceContext($this->getSourceContext()); + } + + // this is mostly useful for \Twig\Error\LoaderError exceptions + // see \Twig\Error\LoaderError + if (-1 === $e->getTemplateLine()) { + $e->guess(); + } + + throw $e; + } catch (\Throwable $e) { + $e = new RuntimeError(\sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $this->getSourceContext(), $e); + $e->guess(); + + throw $e; + } + } + + /** + * @return iterable + */ + public function yieldBlock($name, array $context, array $blocks = [], $useBlocks = true, ?self $templateContext = null): iterable + { + if ($useBlocks && isset($blocks[$name])) { + $template = $blocks[$name][0]; + $block = $blocks[$name][1]; + } elseif (isset($this->blocks[$name])) { + $template = $this->blocks[$name][0]; + $block = $this->blocks[$name][1]; + // expose this template's own blocks so nested block() calls resolve against them when the block is rendered directly (e.g. block(name, template)) + $blocks = array_merge($this->blocks, $blocks); + } else { + $template = null; + $block = null; + } + + // avoid RCEs when sandbox is enabled + if (null !== $template && !$template instanceof self) { + throw new \LogicException('A block must be a method on a \Twig\Template instance.'); + } + + if (null !== $template) { + try { + $template->ensureSecurityChecked(); + yield from $template->$block($context, $blocks); + } catch (Error $e) { + if (!$e->getSourceContext()) { + $e->setSourceContext($template->getSourceContext()); + } + + // this is mostly useful for \Twig\Error\LoaderError exceptions + // see \Twig\Error\LoaderError + if (-1 === $e->getTemplateLine()) { + $e->guess(); + } + + throw $e; + } catch (\Throwable $e) { + $e = new RuntimeError(\sprintf('An exception has been thrown during the rendering of a template ("%s").', $e->getMessage()), -1, $template->getSourceContext(), $e); + $e->guess(); + + throw $e; + } + } elseif ($parent = $this->getParent($context)) { + yield from $parent->unwrap()->yieldBlock($name, $context, array_merge($this->blocks, $blocks), false, $templateContext ?? $this); + } elseif (isset($blocks[$name])) { + throw new RuntimeError(\sprintf('Block "%s" should not call parent() in "%s" as the block does not exist in the parent template "%s".', $name, $blocks[$name][0]->getTemplateName(), $this->getTemplateName()), -1, $blocks[$name][0]->getSourceContext()); + } else { + throw new RuntimeError(\sprintf('Block "%s" on template "%s" does not exist.', $name, $this->getTemplateName()), -1, ($templateContext ?? $this)->getSourceContext()); + } + } + + /** + * Yields a parent block. + * + * This method is for internal use only and should never be called + * directly. + * + * @param string $name The block name to display from the parent + * @param array $context The context + * @param array $blocks The current set of blocks + * + * @return iterable + */ + public function yieldParentBlock($name, array $context, array $blocks = []): iterable + { + if (isset($this->traits[$name])) { + yield from $this->traits[$name][0]->yieldBlock($this->traitAliases[$name] ?? $name, $context, $blocks, false); + } elseif ($parent = $this->getParent($context)) { + yield from $parent->unwrap()->yieldBlock($name, $context, $blocks, false); + } else { + throw new RuntimeError(\sprintf('The template has no parent and no traits defining the "%s" block.', $name), -1, $this->getSourceContext()); + } + } + + protected function hasMacro(string $name, array $context): bool + { + if (method_exists($this, $name)) { + return true; + } + + if (!$parent = $this->getParent($context)) { + return false; + } + + return $parent->hasMacro($name, $context); + } + + protected function getTemplateForMacro(string $name, array $context, int $line, Source $source): self + { + if (method_exists($this, $name)) { + $this->ensureSecurityChecked(); + + return $this; + } + + $parent = $this; + while ($parent = $parent->getParent($context)) { + if (method_exists($parent, $name)) { + $parent->ensureSecurityChecked(); + + return $parent; + } + } + + throw new RuntimeError(\sprintf('Macro "%s" is not defined in template "%s".', substr($name, \strlen('macro_')), $this->getTemplateName()), $line, $source); + } + + /** + * Runs the sandbox security check against the current sandbox state. + * + * @internal + */ + public function ensureSecurityChecked(): void + { + } + + /** + * Auto-generated method to display the template with the given context. + * + * @param array $context An array of parameters to pass to the template + * @param array $blocks An array of blocks to pass to the template + * + * @return iterable + */ + abstract protected function doDisplay(array $context, array $blocks = []): iterable; +} diff --git a/novaconium/vendor/twig/src/TemplateWrapper.php b/novaconium/vendor/twig/src/TemplateWrapper.php new file mode 100644 index 0000000..265ce3e --- /dev/null +++ b/novaconium/vendor/twig/src/TemplateWrapper.php @@ -0,0 +1,112 @@ + + */ +final class TemplateWrapper +{ + /** + * This method is for internal use only and should never be called + * directly (use Twig\Environment::load() instead). + * + * @internal + */ + public function __construct( + private Environment $env, + private Template $template, + ) { + } + + /** + * @return iterable + */ + public function stream(array $context = []): iterable + { + yield from $this->template->yield($context); + } + + /** + * @return iterable + */ + public function streamBlock(string $name, array $context = []): iterable + { + yield from $this->template->yieldBlock($name, $context); + } + + public function render(array $context = []): string + { + return $this->template->render($context); + } + + /** + * @return void + */ + public function display(array $context = []) + { + // using func_get_args() allows to not expose the blocks argument + // as it should only be used by internal code + $this->template->display($context, \func_get_args()[1] ?? []); + } + + public function hasBlock(string $name, array $context = []): bool + { + return $this->template->hasBlock($name, $context); + } + + /** + * @return string[] An array of defined template block names + */ + public function getBlockNames(array $context = []): array + { + return $this->template->getBlockNames($context); + } + + public function renderBlock(string $name, array $context = []): string + { + return $this->template->renderBlock($name, $context + $this->env->getGlobals()); + } + + /** + * @return void + */ + public function displayBlock(string $name, array $context = []) + { + $context += $this->env->getGlobals(); + foreach ($this->template->yieldBlock($name, $context) as $data) { + echo $data; + } + } + + public function getSourceContext(): Source + { + return $this->template->getSourceContext(); + } + + public function getTemplateName(): string + { + return $this->template->getTemplateName(); + } + + /** + * @internal + * + * @return Template + */ + public function unwrap() + { + return $this->template; + } +} diff --git a/novaconium/vendor/twig/src/Test/IntegrationTestCase.php b/novaconium/vendor/twig/src/Test/IntegrationTestCase.php new file mode 100644 index 0000000..749174b --- /dev/null +++ b/novaconium/vendor/twig/src/Test/IntegrationTestCase.php @@ -0,0 +1,380 @@ + + * @author Karma Dordrak + */ +abstract class IntegrationTestCase extends TestCase +{ + /** + * @deprecated since Twig 3.13, use getFixturesDirectory() instead. + * + * @return string + */ + protected function getFixturesDir() + { + throw new \BadMethodCallException('Not implemented.'); + } + + protected static function getFixturesDirectory(): string + { + throw new \BadMethodCallException('Not implemented.'); + } + + /** + * @return RuntimeLoaderInterface[] + */ + protected function getRuntimeLoaders() + { + return []; + } + + /** + * @return ExtensionInterface[] + */ + protected function getExtensions() + { + return []; + } + + /** + * @return TwigFilter[] + */ + protected function getTwigFilters() + { + return []; + } + + /** + * @return TwigFunction[] + */ + protected function getTwigFunctions() + { + return []; + } + + /** + * @return TwigTest[] + */ + protected function getTwigTests() + { + return []; + } + + /** + * @return array + */ + protected function getUndefinedFilterCallbacks(): array + { + return []; + } + + /** + * @return array + */ + protected function getUndefinedFunctionCallbacks(): array + { + return []; + } + + /** + * @return array + */ + protected function getUndefinedTestCallbacks(): array + { + return []; + } + + /** + * @return array + */ + protected function getUndefinedTokenParserCallbacks(): array + { + return []; + } + + /** + * The annotation feeds PHPUnit < 10; the attribute feeds PHPUnit >= 10 and must point to a static provider, as PHPUnit >= 11 rejects non-static ones. + * + * @dataProvider getTests + * + * @return void + */ + #[DataProvider('provideTests')] + public function testIntegration($file, $message, $condition, $templates, $exception, $outputs, $deprecation = '') + { + $this->doIntegrationTest($file, $message, $condition, $templates, $exception, $outputs, $deprecation); + } + + /** + * @dataProvider getLegacyTests + * + * @group legacy + * + * @return void + */ + #[DataProvider('provideLegacyTests'), Group('legacy')] + public function testLegacyIntegration($file, $message, $condition, $templates, $exception, $outputs, $deprecation = '') + { + $this->doIntegrationTest($file, $message, $condition, $templates, $exception, $outputs, $deprecation); + } + + final public static function provideTests(): iterable + { + return self::assembleTests(false, static::getFixturesDirectory()); + } + + final public static function provideLegacyTests(): iterable + { + return self::assembleTests(true, static::getFixturesDirectory()); + } + + /** + * @return iterable + * + * @final since Twig 3.13 + */ + public function getTests($name, $legacyTests = false) + { + try { + $fixturesDir = static::getFixturesDirectory(); + } catch (\BadMethodCallException) { + trigger_deprecation('twig/twig', '3.13', 'Not overriding "%s::getFixturesDirectory()" in "%s" is deprecated. This method will be abstract in 4.0.', self::class, static::class); + $fixturesDir = $this->getFixturesDir(); + } + + return self::assembleTests($legacyTests, $fixturesDir); + } + + private static function assembleTests(bool $legacyTests, string $fixturesDir): array + { + $fixturesDir = realpath($fixturesDir); + $tests = []; + + foreach (new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($fixturesDir), \RecursiveIteratorIterator::LEAVES_ONLY) as $file) { + if (!preg_match('/\.test$/', $file)) { + continue; + } + + if ($legacyTests xor str_contains($file->getRealpath(), '.legacy.test')) { + continue; + } + + $test = file_get_contents($file->getRealpath()); + + if (preg_match('/--TEST--\s*(.*?)\s*(?:--CONDITION--\s*(.*))?\s*(?:--DEPRECATION--\s*(.*?))?\s*((?:--TEMPLATE(?:\(.*?\))?--(?:.*?))+)\s*(?:--DATA--\s*(.*))?\s*--EXCEPTION--\s*(.*)/sx', $test, $match)) { + $message = $match[1]; + $condition = $match[2]; + $deprecation = $match[3]; + $templates = self::parseTemplates($match[4]); + $exception = $match[6]; + $outputs = [[null, $match[5], null, '']]; + } elseif (preg_match('/--TEST--\s*(.*?)\s*(?:--CONDITION--\s*(.*))?\s*(?:--DEPRECATION--\s*(.*?))?\s*((?:--TEMPLATE(?:\(.*?\))?--(?:.*?))+)--DATA--.*?--EXPECT--.*/s', $test, $match)) { + $message = $match[1]; + $condition = $match[2]; + $deprecation = $match[3]; + $templates = self::parseTemplates($match[4]); + $exception = false; + preg_match_all('/--DATA--(.*?)(?:--CONFIG--(.*?))?--EXPECT--(.*?)(?=\-\-DATA\-\-|$)/s', $test, $outputs, \PREG_SET_ORDER); + } else { + throw new \InvalidArgumentException(\sprintf('Test "%s" is not valid.', str_replace($fixturesDir.'/', '', $file))); + } + + $tests[str_replace($fixturesDir.'/', '', $file)] = [str_replace($fixturesDir.'/', '', $file), $message, $condition, $templates, $exception, $outputs, $deprecation]; + } + + if ($legacyTests && !$tests) { + // add a dummy test to avoid a PHPUnit message + return [['not', '-', '', [], '', []]]; + } + + return $tests; + } + + /** + * @final since Twig 3.13 + * + * @return iterable + */ + public function getLegacyTests() + { + return $this->getTests('testLegacyIntegration', true); + } + + /** + * @return void + */ + protected function doIntegrationTest($file, $message, $condition, $templateSources, $exception, $outputs, $deprecation = '') + { + if (!$outputs) { + // dummy test added by assembleTests() when there is no (legacy) test to run + $this->expectNotToPerformAssertions(); + + return; + } + + if ($condition) { + $ret = ''; + eval('$ret = '.$condition.';'); + if (!$ret) { + $this->markTestSkipped($condition); + } + } + + foreach ($outputs as $i => $match) { + $config = array_merge([ + 'cache' => false, + 'strict_variables' => true, + ], $match[2] ? eval($match[2].';') : []); + // make sure that template are always compiled even if they are the same (useful when testing with more than one data/expect sections) + foreach ($templateSources as $name => $template) { + $templateSources[$name] = $template.str_repeat(' ', $i); + } + $loader = new ArrayLoader($templateSources); + $twig = new Environment($loader, $config); + $twig->addGlobal('global', 'global'); + foreach ($this->getRuntimeLoaders() as $runtimeLoader) { + $twig->addRuntimeLoader($runtimeLoader); + } + + foreach ($this->getExtensions() as $extension) { + $twig->addExtension($extension); + } + + foreach ($this->getTwigFilters() as $filter) { + $twig->addFilter($filter); + } + + foreach ($this->getTwigTests() as $test) { + $twig->addTest($test); + } + + foreach ($this->getTwigFunctions() as $function) { + $twig->addFunction($function); + } + + foreach ($this->getUndefinedFilterCallbacks() as $callback) { + $twig->registerUndefinedFilterCallback($callback); + } + + foreach ($this->getUndefinedFunctionCallbacks() as $callback) { + $twig->registerUndefinedFunctionCallback($callback); + } + + foreach ($this->getUndefinedTestCallbacks() as $callback) { + $twig->registerUndefinedTestCallback($callback); + } + + foreach ($this->getUndefinedTokenParserCallbacks() as $callback) { + $twig->registerUndefinedTokenParserCallback($callback); + } + + $deprecations = []; + $templates = []; + try { + $prevHandler = set_error_handler(static function ($type, $msg, $file, $line, $context = []) use (&$deprecations, &$prevHandler) { + if (\E_USER_DEPRECATED === $type) { + $deprecations[] = $msg; + + return true; + } + + return $prevHandler ? $prevHandler($type, $msg, $file, $line, $context) : false; + }); + + foreach (array_keys($templateSources) as $templateName) { + $templates[$templateName] = $twig->load($templateName); + } + } catch (\Exception $e) { + if (false !== $exception) { + $message = $e->getMessage(); + $this->assertSame(trim($exception), trim(\sprintf('%s: %s', $e::class, $message))); + $last = substr($message, \strlen($message) - 1); + $this->assertTrue('.' === $last || '?' === $last, 'Exception message must end with a dot or a question mark.'); + + return; + } + + throw new Error(\sprintf('%s: %s', $e::class, $e->getMessage()), -1, null, $e); + } finally { + restore_error_handler(); + } + + $template = $templates['index.twig']; + + try { + $output = trim($template->render(eval($match[1].';')), "\n "); + } catch (\Exception $e) { + if (false !== $exception) { + $this->assertStringMatchesFormat(trim($exception), trim(\sprintf('%s: %s', $e::class, $e->getMessage()))); + + return; + } + + $e = new Error(\sprintf('%s: %s', $e::class, $e->getMessage()), -1, null, $e); + + $output = trim(\sprintf('%s: %s', $e::class, $e->getMessage())); + } + + if (false !== $exception) { + [$class] = explode(':', $exception); + $constraintClass = class_exists('PHPUnit\Framework\Constraint\Exception') ? 'PHPUnit\Framework\Constraint\Exception' : 'PHPUnit_Framework_Constraint_Exception'; + $this->assertThat(null, new $constraintClass($class)); + } + + $expected = trim($match[3], "\n "); + + if ($expected !== $output) { + printf("Compiled templates that failed on case %d:\n", $i + 1); + + foreach (array_keys($templateSources) as $name) { + echo "Template: $name\n"; + echo $twig->compile($twig->parse($twig->tokenize($twig->getLoader()->getSourceContext($name)))); + } + } + $this->assertEquals($expected, $output, $message.' (in '.$file.')'); + + $this->assertSame($deprecation, implode("\n", $deprecations)); + } + } + + /** + * @return array + */ + protected static function parseTemplates($test) + { + $templates = []; + preg_match_all('/--TEMPLATE(?:\((.*?)\))?--(.*?)(?=\-\-TEMPLATE|$)/s', $test, $matches, \PREG_SET_ORDER); + foreach ($matches as $match) { + $templates[$match[1] ?: 'index.twig'] = $match[2]; + } + + return $templates; + } +} diff --git a/novaconium/vendor/twig/src/Test/NodeTestCase.php b/novaconium/vendor/twig/src/Test/NodeTestCase.php new file mode 100644 index 0000000..3ce58b3 --- /dev/null +++ b/novaconium/vendor/twig/src/Test/NodeTestCase.php @@ -0,0 +1,144 @@ + + */ + public function getTests() + { + return []; + } + + /** + * @return iterable + */ + public static function provideTests(): iterable + { + trigger_deprecation('twig/twig', '3.13', 'Not implementing "%s()" in "%s" is deprecated. This method will be abstract in 4.0.', __METHOD__, static::class); + + return []; + } + + /** + * The non-static "getTests" is intentionally not mirrored as an attribute: PHPUnit >= 11 rejects non-static providers, so PHPUnit >= 10 relies on the static "provideTests" instead. + * + * @dataProvider getTests + * @dataProvider provideTests + * + * @return void + */ + #[DataProvider('provideTests')] + public function testCompile($node, $source, $environment = null, $isPattern = false) + { + $this->assertNodeCompilation($source, $node, $environment, $isPattern); + } + + /** + * @return void + */ + public function assertNodeCompilation($source, Node $node, ?Environment $environment = null, $isPattern = false) + { + $compiler = $this->getCompiler($environment); + $compiler->compile($node); + + if ($isPattern) { + $this->assertStringMatchesFormat($source, trim($compiler->getSource())); + } else { + $this->assertEquals($source, trim($compiler->getSource())); + } + } + + /** + * @return Compiler + */ + protected function getCompiler(?Environment $environment = null) + { + return new Compiler($environment ?? $this->getEnvironment()); + } + + /** + * @return Environment + * + * @final since Twig 3.13 + */ + protected function getEnvironment() + { + return $this->currentEnv ??= static::createEnvironment(); + } + + protected static function createEnvironment(): Environment + { + return new Environment(new ArrayLoader()); + } + + /** + * @return string + * + * @deprecated since Twig 3.13, use createVariableGetter() instead. + */ + protected function getVariableGetter($name, $line = false) + { + trigger_deprecation('twig/twig', '3.13', 'Method "%s()" is deprecated, use "createVariableGetter()" instead.', __METHOD__); + + return self::createVariableGetter($name, $line); + } + + final protected static function createVariableGetter(string $name, bool $line = false): string + { + $line = $line > 0 ? "// line $line\n" : ''; + + return \sprintf('%s($context["%s"] ?? null)', $line, $name); + } + + /** + * @return string + * + * @deprecated since Twig 3.13, use createAttributeGetter() instead. + */ + protected function getAttributeGetter() + { + trigger_deprecation('twig/twig', '3.13', 'Method "%s()" is deprecated, use "createAttributeGetter()" instead.', __METHOD__); + + return self::createAttributeGetter(); + } + + final protected static function createAttributeGetter(): string + { + return 'CoreExtension::getAttribute($this->env, $this->source, '; + } + + /** @beforeClass */ + #[BeforeClass] + final public static function checkDataProvider(): void + { + $r = new \ReflectionMethod(static::class, 'getTests'); + if (self::class !== $r->getDeclaringClass()->getName()) { + trigger_deprecation('twig/twig', '3.13', 'Implementing "%s::getTests()" in "%s" is deprecated, implement "provideTests()" instead.', self::class, static::class); + } + } +} diff --git a/novaconium/vendor/twig/src/Token.php b/novaconium/vendor/twig/src/Token.php new file mode 100644 index 0000000..0c0e46a --- /dev/null +++ b/novaconium/vendor/twig/src/Token.php @@ -0,0 +1,256 @@ + + */ +final class Token +{ + public const EOF_TYPE = -1; + public const TEXT_TYPE = 0; + public const BLOCK_START_TYPE = 1; + public const VAR_START_TYPE = 2; + public const BLOCK_END_TYPE = 3; + public const VAR_END_TYPE = 4; + public const NAME_TYPE = 5; + public const NUMBER_TYPE = 6; + public const STRING_TYPE = 7; + public const OPERATOR_TYPE = 8; + public const PUNCTUATION_TYPE = 9; + public const INTERPOLATION_START_TYPE = 10; + public const INTERPOLATION_END_TYPE = 11; + /** + * @deprecated since Twig 3.21, "arrow" is now an operator + */ + public const ARROW_TYPE = 12; + /** + * @deprecated since Twig 3.21, "spread" is now an operator + */ + public const SPREAD_TYPE = 13; + + /** + * @param non-negative-int|null $offset + */ + public function __construct( + private int $type, + private $value, + private int $lineno, + private ?int $offset = null, + ) { + if (self::ARROW_TYPE === $type) { + trigger_deprecation('twig/twig', '3.21', 'The "%s" token type is deprecated, "arrow" is now an operator.', self::ARROW_TYPE); + } + if (self::SPREAD_TYPE === $type) { + trigger_deprecation('twig/twig', '3.21', 'The "%s" token type is deprecated, "spread" is now an operator.', self::SPREAD_TYPE); + } + } + + public function __toString(): string + { + return \sprintf('%s(%s)', self::typeToString($this->type, true), $this->value); + } + + /** + * Tests the current token for a type and/or a value. + * + * Parameters may be: + * * just type + * * type and value (or array of possible values) + * * just value (or array of possible values) (NAME_TYPE is used as type) + * + * @param array|string|int $type The type to test + * @param array|string|null $values The token value + */ + public function test($type, $values = null): bool + { + if (null === $values && !\is_int($type)) { + $values = $type; + $type = self::NAME_TYPE; + } + + if (self::ARROW_TYPE === $type) { + trigger_deprecation('twig/twig', '3.21', 'The "%s" token type is deprecated, "arrow" is now an operator.', self::typeToEnglish(self::ARROW_TYPE)); + + return self::OPERATOR_TYPE === $this->type && '=>' === $this->value; + } + if (self::SPREAD_TYPE === $type) { + trigger_deprecation('twig/twig', '3.21', 'The "%s" token type is deprecated, "spread" is now an operator.', self::typeToEnglish(self::SPREAD_TYPE)); + + return self::OPERATOR_TYPE === $this->type && '...' === $this->value; + } + + $typeMatches = $this->type === $type; + if ($typeMatches && self::PUNCTUATION_TYPE === $type && \in_array($this->value, ['(', '[', '|', '.', '?', '?:'], true) && $values) { + foreach ((array) $values as $value) { + if (\in_array($value, ['(', '[', '|', '.', '?', '?:'], true)) { + trigger_deprecation('twig/twig', '3.21', 'The "%s" token is now an "%s" token instead of a "%s" one.', $this->value, self::typeToEnglish(self::OPERATOR_TYPE), $this->toEnglish()); + + break; + } + } + } + if (!$typeMatches) { + if (self::OPERATOR_TYPE === $type && self::PUNCTUATION_TYPE === $this->type) { + if ($values) { + foreach ((array) $values as $value) { + if (\in_array($value, ['(', '[', '|', '.', '?', '?:'], true)) { + $typeMatches = true; + + break; + } + } + } else { + $typeMatches = true; + } + } + } + + return $typeMatches && ( + null === $values + || (\is_array($values) && \in_array($this->value, $values, true)) + || $this->value == $values + ); + } + + public function getLine(): int + { + return $this->lineno; + } + + /** + * Returns the 0-based byte offset of the token in the source code. + * + * Returns null for tokens that are not tied to a source position (e.g. + * tokens synthesized by a token parser). + * + * @return non-negative-int|null + */ + public function getOffset(): ?int + { + return $this->offset; + } + + /** + * @deprecated since Twig 3.19 + */ + public function getType(): int + { + trigger_deprecation('twig/twig', '3.19', \sprintf('The "%s()" method is deprecated.', __METHOD__)); + + return $this->type; + } + + public function getValue() + { + return $this->value; + } + + public function toEnglish(): string + { + return self::typeToEnglish($this->type); + } + + public static function typeToString(int $type, bool $short = false): string + { + switch ($type) { + case self::EOF_TYPE: + $name = 'EOF_TYPE'; + break; + case self::TEXT_TYPE: + $name = 'TEXT_TYPE'; + break; + case self::BLOCK_START_TYPE: + $name = 'BLOCK_START_TYPE'; + break; + case self::VAR_START_TYPE: + $name = 'VAR_START_TYPE'; + break; + case self::BLOCK_END_TYPE: + $name = 'BLOCK_END_TYPE'; + break; + case self::VAR_END_TYPE: + $name = 'VAR_END_TYPE'; + break; + case self::NAME_TYPE: + $name = 'NAME_TYPE'; + break; + case self::NUMBER_TYPE: + $name = 'NUMBER_TYPE'; + break; + case self::STRING_TYPE: + $name = 'STRING_TYPE'; + break; + case self::OPERATOR_TYPE: + $name = 'OPERATOR_TYPE'; + break; + case self::PUNCTUATION_TYPE: + $name = 'PUNCTUATION_TYPE'; + break; + case self::INTERPOLATION_START_TYPE: + $name = 'INTERPOLATION_START_TYPE'; + break; + case self::INTERPOLATION_END_TYPE: + $name = 'INTERPOLATION_END_TYPE'; + break; + case self::ARROW_TYPE: + $name = 'ARROW_TYPE'; + break; + case self::SPREAD_TYPE: + $name = 'SPREAD_TYPE'; + break; + default: + throw new \LogicException(\sprintf('Token of type "%s" does not exist.', $type)); + } + + return $short ? $name : 'Twig\Token::'.$name; + } + + public static function typeToEnglish(int $type): string + { + switch ($type) { + case self::EOF_TYPE: + return 'end of template'; + case self::TEXT_TYPE: + return 'text'; + case self::BLOCK_START_TYPE: + return 'begin of statement block'; + case self::VAR_START_TYPE: + return 'begin of print statement'; + case self::BLOCK_END_TYPE: + return 'end of statement block'; + case self::VAR_END_TYPE: + return 'end of print statement'; + case self::NAME_TYPE: + return 'name'; + case self::NUMBER_TYPE: + return 'number'; + case self::STRING_TYPE: + return 'string'; + case self::OPERATOR_TYPE: + return 'operator'; + case self::PUNCTUATION_TYPE: + return 'punctuation'; + case self::INTERPOLATION_START_TYPE: + return 'begin of string interpolation'; + case self::INTERPOLATION_END_TYPE: + return 'end of string interpolation'; + case self::ARROW_TYPE: + return 'arrow function'; + case self::SPREAD_TYPE: + return 'spread operator'; + default: + throw new \LogicException(\sprintf('Token of type "%s" does not exist.', $type)); + } + } +} diff --git a/novaconium/vendor/twig/src/TokenParser/AbstractTokenParser.php b/novaconium/vendor/twig/src/TokenParser/AbstractTokenParser.php new file mode 100644 index 0000000..8c07f60 --- /dev/null +++ b/novaconium/vendor/twig/src/TokenParser/AbstractTokenParser.php @@ -0,0 +1,66 @@ + + */ +abstract class AbstractTokenParser implements TokenParserInterface +{ + /** + * @var Parser + */ + protected $parser; + + public function setParser(Parser $parser): void + { + $this->parser = $parser; + } + + public function isAlwaysAllowedInSandbox(): bool + { + return false; + } + + /** + * Parses an assignment expression like "a, b". + */ + protected function parseAssignmentExpression(): Nodes + { + $stream = $this->parser->getStream(); + $targets = []; + while (true) { + $token = $stream->getCurrent(); + if ($stream->test(Token::OPERATOR_TYPE) && preg_match(Lexer::REGEX_NAME, $token->getValue())) { + // in this context, string operators are variable names + $stream->next(); + } else { + $stream->expect(Token::NAME_TYPE, null, 'Only variables can be assigned to'); + } + $targets[] = new AssignContextVariable($token->getValue(), $token->getLine()); + + if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) { + break; + } + } + + return new Nodes($targets); + } +} diff --git a/novaconium/vendor/twig/src/TokenParser/ApplyTokenParser.php b/novaconium/vendor/twig/src/TokenParser/ApplyTokenParser.php new file mode 100644 index 0000000..5b560e7 --- /dev/null +++ b/novaconium/vendor/twig/src/TokenParser/ApplyTokenParser.php @@ -0,0 +1,66 @@ +getLine(); + $ref = new LocalVariable(null, $lineno); + $filter = $ref; + $op = $this->parser->getEnvironment()->getExpressionParsers()->getByClass(FilterExpressionParser::class); + while (true) { + $filter = $op->parse($this->parser, $filter, $this->parser->getCurrentToken()); + if (!$this->parser->getStream()->test(Token::OPERATOR_TYPE, '|')) { + break; + } + $this->parser->getStream()->next(); + } + + $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); + $body = $this->parser->subparse([$this, 'decideApplyEnd'], true); + $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); + + return new Nodes([ + new SetNode(true, $ref, $body, $lineno), + new PrintNode($filter, $lineno), + ], $lineno); + } + + public function decideApplyEnd(Token $token): bool + { + return $token->test('endapply'); + } + + public function getTag(): string + { + return 'apply'; + } +} diff --git a/novaconium/vendor/twig/src/TokenParser/AutoEscapeTokenParser.php b/novaconium/vendor/twig/src/TokenParser/AutoEscapeTokenParser.php new file mode 100644 index 0000000..86feb27 --- /dev/null +++ b/novaconium/vendor/twig/src/TokenParser/AutoEscapeTokenParser.php @@ -0,0 +1,58 @@ +getLine(); + $stream = $this->parser->getStream(); + + if ($stream->test(Token::BLOCK_END_TYPE)) { + $value = 'html'; + } else { + $expr = $this->parser->parseExpression(); + if (!$expr instanceof ConstantExpression) { + throw new SyntaxError('An escaping strategy must be a string or false.', $stream->getCurrent()->getLine(), $stream->getSourceContext()); + } + $value = $expr->getAttribute('value'); + } + + $stream->expect(Token::BLOCK_END_TYPE); + $body = $this->parser->subparse([$this, 'decideBlockEnd'], true); + $stream->expect(Token::BLOCK_END_TYPE); + + return new AutoEscapeNode($value, $body, $lineno); + } + + public function decideBlockEnd(Token $token): bool + { + return $token->test('endautoescape'); + } + + public function getTag(): string + { + return 'autoescape'; + } +} diff --git a/novaconium/vendor/twig/src/TokenParser/BlockTokenParser.php b/novaconium/vendor/twig/src/TokenParser/BlockTokenParser.php new file mode 100644 index 0000000..452b323 --- /dev/null +++ b/novaconium/vendor/twig/src/TokenParser/BlockTokenParser.php @@ -0,0 +1,77 @@ + + * {% block title %}{% endblock %} - My Webpage + * {% endblock %} + * + * @internal + */ +final class BlockTokenParser extends AbstractTokenParser +{ + public function parse(Token $token): Node + { + $lineno = $token->getLine(); + $stream = $this->parser->getStream(); + $name = $stream->expect(Token::NAME_TYPE)->getValue(); + $this->parser->setBlock($name, $block = new BlockNode($name, new EmptyNode(), $lineno)); + $this->parser->pushLocalScope(); + $this->parser->pushBlockStack($name); + + if ($stream->nextIf(Token::BLOCK_END_TYPE)) { + $body = $this->parser->subparse([$this, 'decideBlockEnd'], true); + if ($token = $stream->nextIf(Token::NAME_TYPE)) { + $value = $token->getValue(); + + if ($value != $name) { + throw new SyntaxError(\sprintf('Expected endblock for block "%s" (but "%s" given).', $name, $value), $stream->getCurrent()->getLine(), $stream->getSourceContext()); + } + } + } else { + $body = new Nodes([ + new PrintNode($this->parser->parseExpression(), $lineno), + ]); + } + $stream->expect(Token::BLOCK_END_TYPE); + + $block->setNode('body', $body); + $this->parser->popBlockStack(); + $this->parser->popLocalScope(); + + return new BlockReferenceNode($name, $lineno); + } + + public function decideBlockEnd(Token $token): bool + { + return $token->test('endblock'); + } + + public function getTag(): string + { + return 'block'; + } +} diff --git a/novaconium/vendor/twig/src/TokenParser/DeprecatedTokenParser.php b/novaconium/vendor/twig/src/TokenParser/DeprecatedTokenParser.php new file mode 100644 index 0000000..df1ba38 --- /dev/null +++ b/novaconium/vendor/twig/src/TokenParser/DeprecatedTokenParser.php @@ -0,0 +1,65 @@ + + * + * @internal + */ +final class DeprecatedTokenParser extends AbstractTokenParser +{ + public function parse(Token $token): Node + { + $stream = $this->parser->getStream(); + $expr = $this->parser->parseExpression(); + $node = new DeprecatedNode($expr, $token->getLine()); + + while ($stream->test(Token::NAME_TYPE)) { + $k = $stream->getCurrent()->getValue(); + $stream->next(); + $stream->expect(Token::OPERATOR_TYPE, '='); + + switch ($k) { + case 'package': + $node->setNode('package', $this->parser->parseExpression()); + break; + case 'version': + $node->setNode('version', $this->parser->parseExpression()); + break; + default: + throw new SyntaxError(\sprintf('Unknown "%s" option.', $k), $stream->getCurrent()->getLine(), $stream->getSourceContext()); + } + } + + $stream->expect(Token::BLOCK_END_TYPE); + + return $node; + } + + public function getTag(): string + { + return 'deprecated'; + } +} diff --git a/novaconium/vendor/twig/src/TokenParser/DoTokenParser.php b/novaconium/vendor/twig/src/TokenParser/DoTokenParser.php new file mode 100644 index 0000000..ca9d03d --- /dev/null +++ b/novaconium/vendor/twig/src/TokenParser/DoTokenParser.php @@ -0,0 +1,38 @@ +parser->parseExpression(); + + $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); + + return new DoNode($expr, $token->getLine()); + } + + public function getTag(): string + { + return 'do'; + } +} diff --git a/novaconium/vendor/twig/src/TokenParser/EmbedTokenParser.php b/novaconium/vendor/twig/src/TokenParser/EmbedTokenParser.php new file mode 100644 index 0000000..fa27910 --- /dev/null +++ b/novaconium/vendor/twig/src/TokenParser/EmbedTokenParser.php @@ -0,0 +1,73 @@ +parser->getStream(); + + $parent = $this->parser->parseExpression(); + + [$variables, $only, $ignoreMissing] = $this->parseArguments(); + + $parentToken = $fakeParentToken = new Token(Token::STRING_TYPE, '__parent__', $token->getLine()); + if ($parent instanceof ConstantExpression) { + $parentToken = new Token(Token::STRING_TYPE, $parent->getAttribute('value'), $token->getLine()); + } elseif ($parent instanceof ContextVariable) { + $parentToken = new Token(Token::NAME_TYPE, $parent->getAttribute('name'), $token->getLine()); + } + + // inject a fake parent to make the parent() function work + $stream->injectTokens([ + new Token(Token::BLOCK_START_TYPE, '', $token->getLine()), + new Token(Token::NAME_TYPE, 'extends', $token->getLine()), + $parentToken, + new Token(Token::BLOCK_END_TYPE, '', $token->getLine()), + ]); + + $module = $this->parser->parse($stream, [$this, 'decideBlockEnd'], true); + + // override the parent with the correct one + if ($fakeParentToken === $parentToken) { + $module->setNode('parent', $parent); + } + + $this->parser->embedTemplate($module); + + $stream->expect(Token::BLOCK_END_TYPE); + + return new EmbedNode($module->getTemplateName(), $module->getAttribute('index'), $variables, $only, $ignoreMissing, $token->getLine()); + } + + public function decideBlockEnd(Token $token): bool + { + return $token->test('endembed'); + } + + public function getTag(): string + { + return 'embed'; + } +} diff --git a/novaconium/vendor/twig/src/TokenParser/ExtendsTokenParser.php b/novaconium/vendor/twig/src/TokenParser/ExtendsTokenParser.php new file mode 100644 index 0000000..cba2927 --- /dev/null +++ b/novaconium/vendor/twig/src/TokenParser/ExtendsTokenParser.php @@ -0,0 +1,41 @@ +parser->getStream(); + $this->parser->setParent($this->parser->parseExpression(), false); + $stream->expect(Token::BLOCK_END_TYPE); + + return new ConfigNode($token->getLine()); + } + + public function getTag(): string + { + return 'extends'; + } +} diff --git a/novaconium/vendor/twig/src/TokenParser/FlushTokenParser.php b/novaconium/vendor/twig/src/TokenParser/FlushTokenParser.php new file mode 100644 index 0000000..0d23887 --- /dev/null +++ b/novaconium/vendor/twig/src/TokenParser/FlushTokenParser.php @@ -0,0 +1,38 @@ +parser->getStream()->expect(Token::BLOCK_END_TYPE); + + return new FlushNode($token->getLine()); + } + + public function getTag(): string + { + return 'flush'; + } +} diff --git a/novaconium/vendor/twig/src/TokenParser/ForTokenParser.php b/novaconium/vendor/twig/src/TokenParser/ForTokenParser.php new file mode 100644 index 0000000..21166fc --- /dev/null +++ b/novaconium/vendor/twig/src/TokenParser/ForTokenParser.php @@ -0,0 +1,80 @@ + + * {% for user in users %} + *
  • {{ user.username|e }}
  • + * {% endfor %} + * + * + * @internal + */ +final class ForTokenParser extends AbstractTokenParser +{ + public function parse(Token $token): Node + { + $lineno = $token->getLine(); + $stream = $this->parser->getStream(); + $targets = $this->parseAssignmentExpression(); + $stream->expect(Token::OPERATOR_TYPE, 'in'); + $seq = $this->parser->parseExpression(); + + $stream->expect(Token::BLOCK_END_TYPE); + $body = $this->parser->subparse([$this, 'decideForFork']); + if ('else' == $stream->next()->getValue()) { + $elseLineno = $stream->getCurrent()->getLine(); + $stream->expect(Token::BLOCK_END_TYPE); + $else = new ForElseNode($this->parser->subparse([$this, 'decideForEnd'], true), $elseLineno); + } else { + $else = null; + } + $stream->expect(Token::BLOCK_END_TYPE); + + if (\count($targets) > 1) { + $keyTarget = $targets->getNode('0'); + $keyTarget = new AssignContextVariable($keyTarget->getAttribute('name'), $keyTarget->getTemplateLine()); + $valueTarget = $targets->getNode('1'); + } else { + $keyTarget = new AssignContextVariable('_key', $lineno); + $valueTarget = $targets->getNode('0'); + } + $valueTarget = new AssignContextVariable($valueTarget->getAttribute('name'), $valueTarget->getTemplateLine()); + + return new ForNode($keyTarget, $valueTarget, $seq, null, $body, $else, $lineno); + } + + public function decideForFork(Token $token): bool + { + return $token->test(['else', 'endfor']); + } + + public function decideForEnd(Token $token): bool + { + return $token->test('endfor'); + } + + public function getTag(): string + { + return 'for'; + } +} diff --git a/novaconium/vendor/twig/src/TokenParser/FromTokenParser.php b/novaconium/vendor/twig/src/TokenParser/FromTokenParser.php new file mode 100644 index 0000000..1c80a17 --- /dev/null +++ b/novaconium/vendor/twig/src/TokenParser/FromTokenParser.php @@ -0,0 +1,69 @@ +parser->parseExpression(); + $stream = $this->parser->getStream(); + $stream->expect(Token::NAME_TYPE, 'import'); + + $targets = []; + while (true) { + $name = $stream->expect(Token::NAME_TYPE)->getValue(); + + if ($stream->nextIf('as')) { + $alias = new AssignContextVariable($stream->expect(Token::NAME_TYPE)->getValue(), $token->getLine()); + } else { + $alias = new AssignContextVariable($name, $token->getLine()); + } + + $targets[$name] = $alias; + + if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) { + break; + } + } + + $stream->expect(Token::BLOCK_END_TYPE); + + $internalRef = new AssignTemplateVariable(new TemplateVariable(null, $token->getLine()), $this->parser->isMainScope()); + $node = new ImportNode($macro, $internalRef, $token->getLine()); + + foreach ($targets as $name => $alias) { + $this->parser->addImportedSymbol('function', $alias->getAttribute('name'), 'macro_'.$name, $internalRef); + } + + return $node; + } + + public function getTag(): string + { + return 'from'; + } +} diff --git a/novaconium/vendor/twig/src/TokenParser/GuardTokenParser.php b/novaconium/vendor/twig/src/TokenParser/GuardTokenParser.php new file mode 100644 index 0000000..eb48865 --- /dev/null +++ b/novaconium/vendor/twig/src/TokenParser/GuardTokenParser.php @@ -0,0 +1,79 @@ +parser->getStream(); + $typeToken = $stream->expect(Token::NAME_TYPE); + if (!\in_array($typeToken->getValue(), ['function', 'filter', 'test'], true)) { + throw new SyntaxError(\sprintf('Supported guard types are function, filter and test, "%s" given.', $typeToken->getValue()), $typeToken->getLine(), $stream->getSourceContext()); + } + $method = 'get'.$typeToken->getValue(); + + $nameToken = $stream->expect(Token::NAME_TYPE); + $name = $nameToken->getValue(); + if ('test' === $typeToken->getValue() && $stream->test(Token::NAME_TYPE)) { + // try 2-words tests + $name .= ' '.$stream->getCurrent()->getValue(); + $stream->next(); + } + + try { + $exists = null !== $this->parser->getEnvironment()->$method($name); + } catch (SyntaxError) { + $exists = false; + } + + $stream->expect(Token::BLOCK_END_TYPE); + if ($exists) { + $body = $this->parser->subparse([$this, 'decideGuardFork']); + } else { + $body = new EmptyNode(); + $this->parser->subparseIgnoreUnknownTwigCallables([$this, 'decideGuardFork']); + } + $else = new EmptyNode(); + if ('else' === $stream->next()->getValue()) { + $stream->expect(Token::BLOCK_END_TYPE); + $else = $this->parser->subparse([$this, 'decideGuardEnd'], true); + } + $stream->expect(Token::BLOCK_END_TYPE); + + return new Nodes([$exists ? $body : $else]); + } + + public function decideGuardFork(Token $token): bool + { + return $token->test(['else', 'endguard']); + } + + public function decideGuardEnd(Token $token): bool + { + return $token->test(['endguard']); + } + + public function getTag(): string + { + return 'guard'; + } +} diff --git a/novaconium/vendor/twig/src/TokenParser/IfTokenParser.php b/novaconium/vendor/twig/src/TokenParser/IfTokenParser.php new file mode 100644 index 0000000..4e3588e --- /dev/null +++ b/novaconium/vendor/twig/src/TokenParser/IfTokenParser.php @@ -0,0 +1,90 @@ + + * {% for user in users %} + *
  • {{ user.username|e }}
  • + * {% endfor %} + * + * {% endif %} + * + * @internal + */ +final class IfTokenParser extends AbstractTokenParser +{ + public function parse(Token $token): Node + { + $lineno = $token->getLine(); + $expr = $this->parser->parseExpression(); + $stream = $this->parser->getStream(); + $stream->expect(Token::BLOCK_END_TYPE); + $body = $this->parser->subparse([$this, 'decideIfFork']); + $tests = [$expr, $body]; + $else = null; + + $end = false; + while (!$end) { + switch ($stream->next()->getValue()) { + case 'else': + $stream->expect(Token::BLOCK_END_TYPE); + $else = $this->parser->subparse([$this, 'decideIfEnd']); + break; + + case 'elseif': + $expr = $this->parser->parseExpression(); + $stream->expect(Token::BLOCK_END_TYPE); + $body = $this->parser->subparse([$this, 'decideIfFork']); + $tests[] = $expr; + $tests[] = $body; + break; + + case 'endif': + $end = true; + break; + + default: + throw new SyntaxError(\sprintf('Unexpected end of template. Twig was looking for the following tags "else", "elseif", or "endif" to close the "if" block started at line %d).', $lineno), $stream->getCurrent()->getLine(), $stream->getSourceContext()); + } + } + + $stream->expect(Token::BLOCK_END_TYPE); + + return new IfNode(new Nodes($tests), $else, $lineno); + } + + public function decideIfFork(Token $token): bool + { + return $token->test(['elseif', 'else', 'endif']); + } + + public function decideIfEnd(Token $token): bool + { + return $token->test(['endif']); + } + + public function getTag(): string + { + return 'if'; + } +} diff --git a/novaconium/vendor/twig/src/TokenParser/ImportTokenParser.php b/novaconium/vendor/twig/src/TokenParser/ImportTokenParser.php new file mode 100644 index 0000000..6dcb766 --- /dev/null +++ b/novaconium/vendor/twig/src/TokenParser/ImportTokenParser.php @@ -0,0 +1,45 @@ +parser->parseExpression(); + $this->parser->getStream()->expect(Token::NAME_TYPE, 'as'); + $name = $this->parser->getStream()->expect(Token::NAME_TYPE)->getValue(); + $var = new AssignTemplateVariable(new TemplateVariable($name, $token->getLine()), $this->parser->isMainScope()); + $this->parser->getStream()->expect(Token::BLOCK_END_TYPE); + $this->parser->addImportedSymbol('template', $name); + + return new ImportNode($macro, $var, $token->getLine()); + } + + public function getTag(): string + { + return 'import'; + } +} diff --git a/novaconium/vendor/twig/src/TokenParser/IncludeTokenParser.php b/novaconium/vendor/twig/src/TokenParser/IncludeTokenParser.php new file mode 100644 index 0000000..55ac151 --- /dev/null +++ b/novaconium/vendor/twig/src/TokenParser/IncludeTokenParser.php @@ -0,0 +1,73 @@ +parser->parseExpression(); + + [$variables, $only, $ignoreMissing] = $this->parseArguments(); + + return new IncludeNode($expr, $variables, $only, $ignoreMissing, $token->getLine()); + } + + /** + * @return array{0: ?AbstractExpression, 1: bool, 2: bool} + */ + protected function parseArguments() + { + $stream = $this->parser->getStream(); + + $ignoreMissing = false; + if ($stream->nextIf(Token::NAME_TYPE, 'ignore')) { + $stream->expect(Token::NAME_TYPE, 'missing'); + + $ignoreMissing = true; + } + + $variables = null; + if ($stream->nextIf(Token::NAME_TYPE, 'with')) { + $variables = $this->parser->parseExpression(); + } + + $only = false; + if ($stream->nextIf(Token::NAME_TYPE, 'only')) { + $only = true; + } + + $stream->expect(Token::BLOCK_END_TYPE); + + return [$variables, $only, $ignoreMissing]; + } + + public function getTag(): string + { + return 'include'; + } +} diff --git a/novaconium/vendor/twig/src/TokenParser/MacroTokenParser.php b/novaconium/vendor/twig/src/TokenParser/MacroTokenParser.php new file mode 100644 index 0000000..7531fe0 --- /dev/null +++ b/novaconium/vendor/twig/src/TokenParser/MacroTokenParser.php @@ -0,0 +1,123 @@ + + * {% endmacro %} + * + * @internal + */ +final class MacroTokenParser extends AbstractTokenParser +{ + public function parse(Token $token): Node + { + $lineno = $token->getLine(); + $stream = $this->parser->getStream(); + $name = $stream->expect(Token::NAME_TYPE)->getValue(); + $arguments = $this->parseDefinition(); + + $stream->expect(Token::BLOCK_END_TYPE); + $this->parser->pushLocalScope(); + $body = $this->parser->subparse([$this, 'decideBlockEnd'], true); + if ($token = $stream->nextIf(Token::NAME_TYPE)) { + $value = $token->getValue(); + + if ($value != $name) { + throw new SyntaxError(\sprintf('Expected endmacro for macro "%s" (but "%s" given).', $name, $value), $stream->getCurrent()->getLine(), $stream->getSourceContext()); + } + } + $this->parser->popLocalScope(); + $stream->expect(Token::BLOCK_END_TYPE); + + $this->parser->setMacro($name, new MacroNode($name, new BodyNode([$body]), $arguments, $lineno)); + + return new ConfigNode($lineno); + } + + public function decideBlockEnd(Token $token): bool + { + return $token->test('endmacro'); + } + + public function getTag(): string + { + return 'macro'; + } + + private function parseDefinition(): ArrayExpression + { + $arguments = new ArrayExpression([], $this->parser->getCurrentToken()->getLine()); + $stream = $this->parser->getStream(); + $stream->expect(Token::OPERATOR_TYPE, '(', 'A list of arguments must begin with an opening parenthesis'); + while (!$stream->test(Token::PUNCTUATION_TYPE, ')')) { + if (\count($arguments)) { + $stream->expect(Token::PUNCTUATION_TYPE, ',', 'Arguments must be separated by a comma'); + + // if the comma above was a trailing comma, early exit the argument parse loop + if ($stream->test(Token::PUNCTUATION_TYPE, ')')) { + break; + } + } + + $token = $stream->expect(Token::NAME_TYPE, null, 'An argument must be a name'); + $name = new LocalVariable($token->getValue(), $this->parser->getCurrentToken()->getLine()); + if ($token = $stream->nextIf(Token::OPERATOR_TYPE, '=')) { + $default = $this->parser->parseExpression(); + } else { + $default = new ConstantExpression(null, $this->parser->getCurrentToken()->getLine()); + $default->setAttribute('is_implicit', true); + } + + if (!$this->checkConstantExpression($default)) { + throw new SyntaxError('A default value for an argument must be a constant (a boolean, a string, a number, a sequence, or a mapping).', $token->getLine(), $stream->getSourceContext()); + } + $arguments->addElement($default, $name); + } + $stream->expect(Token::PUNCTUATION_TYPE, ')', 'A list of arguments must be closed by a parenthesis'); + + return $arguments; + } + + // checks that the node only contains "constant" elements + private function checkConstantExpression(Node $node): bool + { + if (!($node instanceof ConstantExpression || $node instanceof ArrayExpression + || $node instanceof NegUnary || $node instanceof PosUnary + )) { + return false; + } + + foreach ($node as $n) { + if (!$this->checkConstantExpression($n)) { + return false; + } + } + + return true; + } +} diff --git a/novaconium/vendor/twig/src/TokenParser/SandboxTokenParser.php b/novaconium/vendor/twig/src/TokenParser/SandboxTokenParser.php new file mode 100644 index 0000000..e6f4a63 --- /dev/null +++ b/novaconium/vendor/twig/src/TokenParser/SandboxTokenParser.php @@ -0,0 +1,72 @@ +parser->getStream(); + trigger_deprecation('twig/twig', '3.15', \sprintf('The "sandbox" tag is deprecated in "%s" at line %d.', $stream->getSourceContext()->getName(), $token->getLine())); + + $stream->expect(Token::BLOCK_END_TYPE); + $body = $this->parser->subparse([$this, 'decideBlockEnd'], true); + $stream->expect(Token::BLOCK_END_TYPE); + + // in a sandbox tag, only include tags are allowed + if ($body instanceof IncludeNode) { + $body->setAttribute('sandboxed', true); + } else { + foreach ($body as $node) { + if ($node instanceof TextNode && ctype_space($node->getAttribute('data'))) { + continue; + } + + if (!$node instanceof IncludeNode) { + throw new SyntaxError('Only "include" tags are allowed within a "sandbox" section.', $node->getTemplateLine(), $stream->getSourceContext()); + } + + $node->setAttribute('sandboxed', true); + } + } + + return new SandboxNode($body, $token->getLine()); + } + + public function decideBlockEnd(Token $token): bool + { + return $token->test('endsandbox'); + } + + public function getTag(): string + { + return 'sandbox'; + } +} diff --git a/novaconium/vendor/twig/src/TokenParser/SetTokenParser.php b/novaconium/vendor/twig/src/TokenParser/SetTokenParser.php new file mode 100644 index 0000000..1aabbf5 --- /dev/null +++ b/novaconium/vendor/twig/src/TokenParser/SetTokenParser.php @@ -0,0 +1,87 @@ +getLine(); + $stream = $this->parser->getStream(); + $names = $this->parseAssignmentExpression(); + + $capture = false; + if ($stream->nextIf(Token::OPERATOR_TYPE, '=')) { + $values = $this->parseMultitargetExpression(); + + $stream->expect(Token::BLOCK_END_TYPE); + + if (\count($names) !== \count($values)) { + throw new SyntaxError('When using set, you must have the same number of variables and assignments.', $stream->getCurrent()->getLine(), $stream->getSourceContext()); + } + } else { + $capture = true; + + if (\count($names) > 1) { + throw new SyntaxError('When using set with a block, you cannot have a multi-target.', $stream->getCurrent()->getLine(), $stream->getSourceContext()); + } + + $stream->expect(Token::BLOCK_END_TYPE); + + $values = $this->parser->subparse([$this, 'decideBlockEnd'], true); + $stream->expect(Token::BLOCK_END_TYPE); + } + + return new SetNode($capture, $names, $values, $lineno); + } + + public function decideBlockEnd(Token $token): bool + { + return $token->test('endset'); + } + + public function getTag(): string + { + return 'set'; + } + + private function parseMultitargetExpression(): Nodes + { + $targets = []; + while (true) { + $targets[] = $this->parser->parseExpression(); + if (!$this->parser->getStream()->nextIf(Token::PUNCTUATION_TYPE, ',')) { + break; + } + } + + return new Nodes($targets); + } +} diff --git a/novaconium/vendor/twig/src/TokenParser/TokenParserInterface.php b/novaconium/vendor/twig/src/TokenParser/TokenParserInterface.php new file mode 100644 index 0000000..ae78714 --- /dev/null +++ b/novaconium/vendor/twig/src/TokenParser/TokenParserInterface.php @@ -0,0 +1,48 @@ + + * + * @method bool isAlwaysAllowedInSandbox() Whether the tag is always allowed in sandbox mode, even when not explicitly allow-listed. Not implementing this method is deprecated since Twig 3.28, it will be required in 4.0. + */ +interface TokenParserInterface +{ + /** + * Sets the parser associated with this token parser. + */ + public function setParser(Parser $parser): void; + + /** + * Parses a token and returns a node. + * + * @return Node + * + * @throws SyntaxError + */ + public function parse(Token $token); + + /** + * Gets the tag name associated with this token parser. + * + * @return string + */ + public function getTag(); +} diff --git a/novaconium/vendor/twig/src/TokenParser/TypesTokenParser.php b/novaconium/vendor/twig/src/TokenParser/TypesTokenParser.php new file mode 100644 index 0000000..2c7b77c --- /dev/null +++ b/novaconium/vendor/twig/src/TokenParser/TypesTokenParser.php @@ -0,0 +1,89 @@ + + * + * @internal + */ +final class TypesTokenParser extends AbstractTokenParser +{ + public function parse(Token $token): Node + { + $stream = $this->parser->getStream(); + $types = $this->parseSimpleMappingExpression($stream); + $stream->expect(Token::BLOCK_END_TYPE); + + return new TypesNode($types, $token->getLine()); + } + + /** + * @return array + * + * @throws SyntaxError + */ + private function parseSimpleMappingExpression(TokenStream $stream): array + { + $enclosed = null !== $stream->nextIf(Token::PUNCTUATION_TYPE, '{'); + $types = []; + $first = true; + while (!($stream->test(Token::PUNCTUATION_TYPE, '}') || $stream->test(Token::BLOCK_END_TYPE))) { + if (!$first) { + $stream->expect(Token::PUNCTUATION_TYPE, ',', 'A type string must be followed by a comma'); + + // trailing ,? + if ($stream->test(Token::PUNCTUATION_TYPE, '}') || $stream->test(Token::BLOCK_END_TYPE)) { + break; + } + } + $first = false; + + $nameToken = $stream->expect(Token::NAME_TYPE); + + if ($stream->nextIf(Token::OPERATOR_TYPE, '?:')) { + $isOptional = true; + } else { + $isOptional = null !== $stream->nextIf(Token::OPERATOR_TYPE, '?'); + $stream->expect(Token::PUNCTUATION_TYPE, ':', 'A type name must be followed by a colon (:)'); + } + + $valueToken = $stream->expect(Token::STRING_TYPE); + + $types[$nameToken->getValue()] = [ + 'type' => $valueToken->getValue(), + 'optional' => $isOptional, + ]; + } + + if ($enclosed) { + $stream->expect(Token::PUNCTUATION_TYPE, '}', 'An opened mapping is not properly closed'); + } + + return $types; + } + + public function getTag(): string + { + return 'types'; + } +} diff --git a/novaconium/vendor/twig/src/TokenParser/UseTokenParser.php b/novaconium/vendor/twig/src/TokenParser/UseTokenParser.php new file mode 100644 index 0000000..a85b023 --- /dev/null +++ b/novaconium/vendor/twig/src/TokenParser/UseTokenParser.php @@ -0,0 +1,75 @@ +parser->parseExpression(); + $stream = $this->parser->getStream(); + + if (!$template instanceof ConstantExpression) { + throw new SyntaxError('The template references in a "use" statement must be a string.', $stream->getCurrent()->getLine(), $stream->getSourceContext()); + } + + $targets = []; + if ($stream->nextIf('with')) { + while (true) { + $name = $stream->expect(Token::NAME_TYPE)->getValue(); + + $alias = $name; + if ($stream->nextIf('as')) { + $alias = $stream->expect(Token::NAME_TYPE)->getValue(); + } + + $targets[$name] = new ConstantExpression($alias, -1); + + if (!$stream->nextIf(Token::PUNCTUATION_TYPE, ',')) { + break; + } + } + } + + $stream->expect(Token::BLOCK_END_TYPE); + + $this->parser->addTrait(new Nodes(['template' => $template, 'targets' => new Nodes($targets)])); + + return new ConfigNode($token->getLine()); + } + + public function getTag(): string + { + return 'use'; + } +} diff --git a/novaconium/vendor/twig/src/TokenParser/WithTokenParser.php b/novaconium/vendor/twig/src/TokenParser/WithTokenParser.php new file mode 100644 index 0000000..83470d8 --- /dev/null +++ b/novaconium/vendor/twig/src/TokenParser/WithTokenParser.php @@ -0,0 +1,56 @@ + + * + * @internal + */ +final class WithTokenParser extends AbstractTokenParser +{ + public function parse(Token $token): Node + { + $stream = $this->parser->getStream(); + + $variables = null; + $only = false; + if (!$stream->test(Token::BLOCK_END_TYPE)) { + $variables = $this->parser->parseExpression(); + $only = (bool) $stream->nextIf(Token::NAME_TYPE, 'only'); + } + + $stream->expect(Token::BLOCK_END_TYPE); + + $body = $this->parser->subparse([$this, 'decideWithEnd'], true); + + $stream->expect(Token::BLOCK_END_TYPE); + + return new WithNode($body, $variables, $only, $token->getLine()); + } + + public function decideWithEnd(Token $token): bool + { + return $token->test('endwith'); + } + + public function getTag(): string + { + return 'with'; + } +} diff --git a/novaconium/vendor/twig/src/TokenStream.php b/novaconium/vendor/twig/src/TokenStream.php new file mode 100644 index 0000000..2586750 --- /dev/null +++ b/novaconium/vendor/twig/src/TokenStream.php @@ -0,0 +1,132 @@ + + */ +final class TokenStream +{ + private $current = 0; + + public function __construct( + private array $tokens, + private ?Source $source = null, + ) { + if (null === $this->source) { + trigger_deprecation('twig/twig', '3.16', \sprintf('Not passing a "%s" object to "%s" constructor is deprecated.', Source::class, __CLASS__)); + + $this->source = new Source('', ''); + } + } + + public function __toString(): string + { + return implode("\n", $this->tokens); + } + + /** + * @return void + */ + public function injectTokens(array $tokens) + { + $this->tokens = array_merge(\array_slice($this->tokens, 0, $this->current), $tokens, \array_slice($this->tokens, $this->current)); + } + + /** + * Sets the pointer to the next token and returns the old one. + */ + public function next(): Token + { + if (!isset($this->tokens[++$this->current])) { + throw new SyntaxError('Unexpected end of template.', $this->tokens[$this->current - 1]->getLine(), $this->source); + } + + return $this->tokens[$this->current - 1]; + } + + /** + * Tests a token, sets the pointer to the next one and returns it or throws a syntax error. + * + * @return Token|null The next token if the condition is true, null otherwise + */ + public function nextIf($primary, $secondary = null) + { + return $this->tokens[$this->current]->test($primary, $secondary) ? $this->next() : null; + } + + /** + * Tests a token and returns it or throws a syntax error. + */ + public function expect($type, $value = null, ?string $message = null): Token + { + $token = $this->tokens[$this->current]; + if (!$token->test($type, $value)) { + $line = $token->getLine(); + throw new SyntaxError(\sprintf('%sUnexpected token "%s"%s ("%s" expected%s).', + $message ? $message.'. ' : '', + $token->toEnglish(), + $token->getValue() ? \sprintf(' of value "%s"', $token->getValue()) : '', + Token::typeToEnglish($type), $value ? \sprintf(' with value "%s"', $value) : ''), + $line, + $this->source, + columnno: $this->source->getColumn($token->getOffset() ?? -1), + ); + } + $this->next(); + + return $token; + } + + /** + * Looks at the next token. + */ + public function look(int $number = 1): Token + { + if (!isset($this->tokens[$this->current + $number])) { + throw new SyntaxError('Unexpected end of template.', $this->tokens[$this->current + $number - 1]->getLine(), $this->source); + } + + return $this->tokens[$this->current + $number]; + } + + /** + * Tests the current token. + */ + public function test($primary, $secondary = null): bool + { + return $this->tokens[$this->current]->test($primary, $secondary); + } + + /** + * Checks if end of stream was reached. + */ + public function isEOF(): bool + { + return $this->tokens[$this->current]->test(Token::EOF_TYPE); + } + + public function getCurrent(): Token + { + return $this->tokens[$this->current]; + } + + public function getSourceContext(): Source + { + return $this->source; + } +} diff --git a/novaconium/vendor/twig/src/TwigCallableInterface.php b/novaconium/vendor/twig/src/TwigCallableInterface.php new file mode 100644 index 0000000..06b0e2e --- /dev/null +++ b/novaconium/vendor/twig/src/TwigCallableInterface.php @@ -0,0 +1,56 @@ + + * + * @method bool needsIsSandboxed() Whether the callable needs the current sandbox state passed as an argument. Not implementing this method is deprecated since Twig 3.25, it will be required in 4.0. + * @method bool isAlwaysAllowedInSandbox() Whether the callable is always allowed in sandbox mode, even when not explicitly allow-listed. Not implementing this method is deprecated since Twig 3.28, it will be required in 4.0. + */ +interface TwigCallableInterface extends \Stringable +{ + public function getName(): string; + + public function getType(): string; + + public function getDynamicName(): string; + + /** + * @return callable|array{class-string, string}|null + */ + public function getCallable(); + + public function getNodeClass(): string; + + public function needsCharset(): bool; + + public function needsEnvironment(): bool; + + public function needsContext(): bool; + + public function withDynamicArguments(string $name, string $dynamicName, array $arguments): self; + + public function getArguments(): array; + + public function isVariadic(): bool; + + public function isDeprecated(): bool; + + public function getDeprecatingPackage(): string; + + public function getDeprecatedVersion(): string; + + public function getAlternative(): ?string; + + public function getMinimalNumberOfRequiredArguments(): int; +} diff --git a/novaconium/vendor/twig/src/TwigFilter.php b/novaconium/vendor/twig/src/TwigFilter.php new file mode 100644 index 0000000..dece518 --- /dev/null +++ b/novaconium/vendor/twig/src/TwigFilter.php @@ -0,0 +1,74 @@ + + * + * @see https://twig.symfony.com/doc/templates.html#filters + */ +final class TwigFilter extends AbstractTwigCallable +{ + /** + * @param callable|array{class-string, string}|null $callable A callable implementing the filter. If null, you need to overwrite the "node_class" option to customize compilation. + */ + public function __construct(string $name, $callable = null, array $options = []) + { + parent::__construct($name, $callable, $options); + + $this->options = array_merge([ + 'is_safe' => null, + 'is_safe_callback' => null, + 'pre_escape' => null, + 'preserves_safety' => null, + 'node_class' => FilterExpression::class, + ], $this->options); + } + + public function getType(): string + { + return 'filter'; + } + + public function getSafe(Node $filterArgs): ?array + { + if (null !== $this->options['is_safe']) { + return $this->options['is_safe']; + } + + if (null !== $this->options['is_safe_callback']) { + return $this->options['is_safe_callback']($filterArgs); + } + + return []; + } + + public function getPreservesSafety(): array + { + return $this->options['preserves_safety'] ?? []; + } + + public function getPreEscape(): ?string + { + return $this->options['pre_escape']; + } + + public function getMinimalNumberOfRequiredArguments(): int + { + return parent::getMinimalNumberOfRequiredArguments() + 1; + } +} diff --git a/novaconium/vendor/twig/src/TwigFunction.php b/novaconium/vendor/twig/src/TwigFunction.php new file mode 100644 index 0000000..4a10df9 --- /dev/null +++ b/novaconium/vendor/twig/src/TwigFunction.php @@ -0,0 +1,63 @@ + + * + * @see https://twig.symfony.com/doc/templates.html#functions + */ +final class TwigFunction extends AbstractTwigCallable +{ + /** + * @param callable|array{class-string, string}|null $callable A callable implementing the function. If null, you need to overwrite the "node_class" option to customize compilation. + */ + public function __construct(string $name, $callable = null, array $options = []) + { + parent::__construct($name, $callable, $options); + + $this->options = array_merge([ + 'is_safe' => null, + 'is_safe_callback' => null, + 'node_class' => FunctionExpression::class, + 'parser_callable' => null, + ], $this->options); + } + + public function getType(): string + { + return 'function'; + } + + public function getParserCallable(): ?callable + { + return $this->options['parser_callable']; + } + + public function getSafe(Node $functionArgs): ?array + { + if (null !== $this->options['is_safe']) { + return $this->options['is_safe']; + } + + if (null !== $this->options['is_safe_callback']) { + return $this->options['is_safe_callback']($functionArgs); + } + + return []; + } +} diff --git a/novaconium/vendor/twig/src/TwigTest.php b/novaconium/vendor/twig/src/TwigTest.php new file mode 100644 index 0000000..5e58ad8 --- /dev/null +++ b/novaconium/vendor/twig/src/TwigTest.php @@ -0,0 +1,67 @@ + + * + * @see https://twig.symfony.com/doc/templates.html#test-operator + */ +final class TwigTest extends AbstractTwigCallable +{ + /** + * @param callable|array{class-string, string}|null $callable A callable implementing the test. If null, you need to overwrite the "node_class" option to customize compilation. + */ + public function __construct(string $name, $callable = null, array $options = []) + { + parent::__construct($name, $callable, $options); + + $this->options = array_merge([ + 'node_class' => TestExpression::class, + 'one_mandatory_argument' => false, + ], $this->options); + } + + public function getType(): string + { + return 'test'; + } + + public function needsCharset(): bool + { + return false; + } + + public function needsEnvironment(): bool + { + return false; + } + + public function needsContext(): bool + { + return false; + } + + public function hasOneMandatoryArgument(): bool + { + return (bool) $this->options['one_mandatory_argument']; + } + + public function getMinimalNumberOfRequiredArguments(): int + { + return parent::getMinimalNumberOfRequiredArguments() + 1; + } +} diff --git a/novaconium/vendor/twig/src/Util/CallableArgumentsExtractor.php b/novaconium/vendor/twig/src/Util/CallableArgumentsExtractor.php new file mode 100644 index 0000000..e3e1f82 --- /dev/null +++ b/novaconium/vendor/twig/src/Util/CallableArgumentsExtractor.php @@ -0,0 +1,204 @@ + + * + * @internal + */ +final class CallableArgumentsExtractor +{ + private ReflectionCallable $rc; + + public function __construct( + private Node $node, + private TwigCallableInterface $twigCallable, + ) { + $this->rc = new ReflectionCallable($twigCallable); + } + + /** + * @return array + */ + public function extractArguments(Node $arguments): array + { + $extractedArguments = []; + $extractedArgumentNameMap = []; + $named = false; + foreach ($arguments as $name => $node) { + if (!\is_int($name)) { + $named = true; + } elseif ($named) { + throw new SyntaxError(\sprintf('Positional arguments cannot be used after named arguments for %s "%s".', $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); + } + + $extractedArguments[$normalizedName = $this->normalizeName($name)] = $node; + $extractedArgumentNameMap[$normalizedName] = $name; + } + + if (!$named && !$this->twigCallable->isVariadic()) { + $min = $this->twigCallable->getMinimalNumberOfRequiredArguments(); + if (\count($extractedArguments) < $this->rc->getReflector()->getNumberOfRequiredParameters() - $min) { + $argName = $this->toSnakeCase($this->rc->getReflector()->getParameters()[$min + \count($extractedArguments)]->getName()); + + throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $argName, $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); + } + + return $extractedArguments; + } + + if (!$callable = $this->twigCallable->getCallable()) { + if ($named) { + throw new SyntaxError(\sprintf('Named arguments are not supported for %s "%s".', $this->twigCallable->getType(), $this->twigCallable->getName())); + } + + throw new SyntaxError(\sprintf('Arbitrary positional arguments are not supported for %s "%s".', $this->twigCallable->getType(), $this->twigCallable->getName())); + } + + [$callableParameters, $isPhpVariadic] = $this->getCallableParameters(); + $arguments = []; + $callableParameterNames = []; + $missingArguments = []; + $optionalArguments = []; + $pos = 0; + foreach ($callableParameters as $callableParameter) { + $callableParameterName = $callableParameter->name; + if (\PHP_VERSION_ID >= 80000 && 'range' === $callable) { + if ('start' === $callableParameterName) { + $callableParameterName = 'low'; + } elseif ('end' === $callableParameterName) { + $callableParameterName = 'high'; + } + } + + $callableParameterNames[] = $callableParameterName; + $normalizedCallableParameterName = $this->normalizeName($callableParameterName); + + if (\array_key_exists($normalizedCallableParameterName, $extractedArguments)) { + if (\array_key_exists($pos, $extractedArguments)) { + throw new SyntaxError(\sprintf('Argument "%s" is defined twice for %s "%s".', $callableParameterName, $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); + } + + if (\count($missingArguments)) { + throw new SyntaxError(\sprintf( + 'Argument "%s" could not be assigned for %s "%s(%s)" because it is mapped to an internal PHP function which cannot determine default value for optional argument%s "%s".', + $callableParameterName, $this->twigCallable->getType(), $this->twigCallable->getName(), implode(', ', array_map([$this, 'toSnakeCase'], $callableParameterNames)), \count($missingArguments) > 1 ? 's' : '', implode('", "', $missingArguments) + ), $this->node->getTemplateLine(), $this->node->getSourceContext()); + } + + $arguments = array_merge($arguments, $optionalArguments); + $arguments[] = $extractedArguments[$normalizedCallableParameterName]; + unset($extractedArguments[$normalizedCallableParameterName]); + $optionalArguments = []; + } elseif (\array_key_exists($pos, $extractedArguments)) { + $arguments = array_merge($arguments, $optionalArguments); + $arguments[] = $extractedArguments[$pos]; + unset($extractedArguments[$pos]); + $optionalArguments = []; + ++$pos; + } elseif ($callableParameter->isDefaultValueAvailable()) { + $optionalArguments[] = new ConstantExpression($callableParameter->getDefaultValue(), $this->node->getTemplateLine()); + } elseif ($callableParameter->isOptional()) { + if (!$extractedArguments) { + break; + } + + $missingArguments[] = $callableParameterName; + } else { + throw new SyntaxError(\sprintf('Value for argument "%s" is required for %s "%s".', $this->toSnakeCase($callableParameterName), $this->twigCallable->getType(), $this->twigCallable->getName()), $this->node->getTemplateLine(), $this->node->getSourceContext()); + } + } + + if ($this->twigCallable->isVariadic()) { + $arbitraryArguments = $isPhpVariadic ? new VariadicExpression([], $this->node->getTemplateLine()) : new ArrayExpression([], $this->node->getTemplateLine()); + foreach ($extractedArguments as $key => $value) { + if (\is_int($key)) { + $arbitraryArguments->addElement($value); + } else { + $originalKey = $extractedArgumentNameMap[$key]; + if ($originalKey !== $this->toSnakeCase($originalKey)) { + trigger_deprecation('twig/twig', '3.15', \sprintf('Using "snake_case" for variadic arguments is required for a smooth upgrade with Twig 4.0; rename "%s" to "%s" in "%s" at line %d.', $originalKey, $this->toSnakeCase($originalKey), $this->node->getSourceContext()->getName(), $this->node->getTemplateLine())); + } + $arbitraryArguments->addElement($value, new ConstantExpression($this->toSnakeCase($originalKey), $this->node->getTemplateLine())); + // I Twig 4.0, don't convert the key: + // $arbitraryArguments->addElement($value, new ConstantExpression($originalKey, $this->node->getTemplateLine())); + } + unset($extractedArguments[$key]); + } + + if ($arbitraryArguments->count()) { + $arguments = array_merge($arguments, $optionalArguments); + $arguments[] = $arbitraryArguments; + } + } + + if ($extractedArguments) { + $unknownArgument = null; + foreach ($extractedArguments as $extractedArgument) { + if ($extractedArgument instanceof Node) { + $unknownArgument = $extractedArgument; + break; + } + } + + throw new SyntaxError( + \sprintf( + 'Unknown argument%s "%s" for %s "%s(%s)".', + \count($extractedArguments) > 1 ? 's' : '', implode('", "', array_keys($extractedArguments)), $this->twigCallable->getType(), $this->twigCallable->getName(), implode(', ', array_map([$this, 'toSnakeCase'], $callableParameterNames)) + ), + $unknownArgument ? $unknownArgument->getTemplateLine() : $this->node->getTemplateLine(), + $unknownArgument ? $unknownArgument->getSourceContext() : $this->node->getSourceContext() + ); + } + + return $arguments; + } + + private function normalizeName(string $name): string + { + return strtolower(str_replace('_', '', $name)); + } + + private function toSnakeCase(string $name): string + { + return strtolower(preg_replace(['/([A-Z]+)([A-Z][a-z])/', '/([a-z0-9])([A-Z])/'], '\1_\2', $name)); + } + + private function getCallableParameters(): array + { + $parameters = $this->rc->getTwigParameters($this->node->hasNode('node')); + + $isPhpVariadic = false; + if ($this->twigCallable->isVariadic()) { + $argument = end($parameters); + $isArray = $argument && $argument->hasType() && $argument->getType() instanceof \ReflectionNamedType && 'array' === $argument->getType()->getName(); + if ($isArray && $argument->isDefaultValueAvailable() && [] === $argument->getDefaultValue()) { + array_pop($parameters); + } elseif ($argument && $argument->isVariadic()) { + array_pop($parameters); + $isPhpVariadic = true; + } else { + throw new SyntaxError(\sprintf('The last parameter of "%s" for %s "%s" must be an array with default value, eg. "array $arg = []".', $this->rc->getName(), $this->twigCallable->getType(), $this->twigCallable->getName())); + } + } + + return [$parameters, $isPhpVariadic]; + } +} diff --git a/novaconium/vendor/twig/src/Util/CallableParameters.php b/novaconium/vendor/twig/src/Util/CallableParameters.php new file mode 100644 index 0000000..4321bb2 --- /dev/null +++ b/novaconium/vendor/twig/src/Util/CallableParameters.php @@ -0,0 +1,161 @@ + + * + * @internal + */ +final class CallableParameters +{ + /** + * Returns the PHP parameters of a Filter/Function/Test call mapped to its + * template-level arguments. + * + * The parameters Twig injects automatically + * (`needs_charset/environment/context/is_sandboxed`) and the bound + * `arguments` are stripped. For a filter, the first returned parameter is + * the filter's input value. Returns null when reflection fails or the node + * is not a callable expression. + * + * @return list<\ReflectionParameter>|null + */ + public static function fromNode(Node $node, Environment $env): ?array + { + if (!$node instanceof FilterExpression && !$node instanceof FunctionExpression && !$node instanceof TestExpression) { + return null; + } + + $callable = self::resolveTwigCallable($node, $env); + if (null === $callable || null === $callable->getCallable()) { + return null; + } + + try { + return (new ReflectionCallable($callable))->getTwigParameters(); + } catch (\LogicException) { + return null; + } + } + + /** + * Returns true when a PHP parameter type proves the value reaching it + * cannot implicitly string-coerce (directly or by iterating it). + * + * Safe: `int`, `float`, `bool`, `null`, `false`, `true`, `void`, `never`, + * enums, and `final` class types that are neither `Stringable` nor + * `Traversable`. + * + * Unsafe: `string`, `array`, `iterable`, `mixed`, `object`, + * untyped, interfaces, non-final classes, and any type that is + * `Stringable` or `Traversable`. Interfaces and non-final classes are + * open: a subtype could add `Stringable`/`Traversable` and reach the host + * code, bypassing the `__toString` policy, so only a `final` class (enums + * included) is closed enough. + * + * @param \ReflectionClass|null $scope resolves the relative types `self`/`parent`/`static` (which + * older PHP versions report verbatim instead of the declaring class) + */ + public static function isStringCoercionSafe(?\ReflectionType $type, ?\ReflectionClass $scope = null): bool + { + if (null === $type) { + return false; + } + + // A union value is one of its members, so every member must be safe. + if ($type instanceof \ReflectionUnionType) { + foreach ($type->getTypes() as $t) { + if (!self::isStringCoercionSafe($t, $scope)) { + return false; + } + } + + return true; + } + + // An intersection value satisfies all its members at once. A safe + // member is necessarily a final class, which pins the concrete class, + // so the value is that non-coercible class whatever the other members. + if ($type instanceof \ReflectionIntersectionType) { + foreach ($type->getTypes() as $t) { + if (self::isStringCoercionSafe($t, $scope)) { + return true; + } + } + + return false; + } + + if (!$type instanceof \ReflectionNamedType) { + return false; + } + + $name = $type->getName(); + + if ($type->isBuiltin()) { + return match ($name) { + // `null` (e.g. as a union member) cannot have a __toString. + 'null', 'int', 'float', 'bool', 'true', 'false', 'void', 'never' => true, + default => false, // string, array, iterable, object, mixed, callable + }; + } + + // Resolve `self`/`parent`/`static` to the concrete class + // `static` is treated like `self` since a `final` class cannot be subclassed anyway + $class = match ($name) { + 'self', 'static' => $scope, + 'parent' => $scope ? ($scope->getParentClass() ?: null) : null, + default => class_exists($name, false) ? new \ReflectionClass($name) : null, + }; + + // Interfaces and non-final classes are open: a subtype could add + // Stringable/Traversable, so only a final non-coercible class is safe + if (null === $class) { + return false; + } + if (is_a($class->getName(), \Stringable::class, true) || is_a($class->getName(), \Traversable::class, true)) { + return false; + } + + return $class->isFinal(); + } + + private static function resolveTwigCallable(Node $node, Environment $env): ?TwigCallableInterface + { + if ($node->hasAttribute('twig_callable')) { + return $node->getAttribute('twig_callable'); + } + if (!$node->hasAttribute('name')) { + return null; + } + $name = $node->getAttribute('name'); + try { + return match (true) { + $node instanceof FilterExpression => $env->getFilter($name), + $node instanceof FunctionExpression => $env->getFunction($name), + $node instanceof TestExpression => $env->getTest($name), + }; + } catch (\Throwable) { + return null; + } + } +} diff --git a/novaconium/vendor/twig/src/Util/DeprecationCollector.php b/novaconium/vendor/twig/src/Util/DeprecationCollector.php new file mode 100644 index 0000000..64cbba5 --- /dev/null +++ b/novaconium/vendor/twig/src/Util/DeprecationCollector.php @@ -0,0 +1,77 @@ + + */ +final class DeprecationCollector +{ + public function __construct( + private Environment $twig, + ) { + } + + /** + * Returns deprecations for templates contained in a directory. + * + * @param string $dir A directory where templates are stored + * @param string $ext Limit the loaded templates by extension + * + * @return array An array of deprecations + */ + public function collectDir(string $dir, string $ext = '.twig'): array + { + $iterator = new \RegexIterator( + new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($dir), \RecursiveIteratorIterator::LEAVES_ONLY + ), '{'.preg_quote($ext).'$}' + ); + + return $this->collect(new TemplateDirIterator($iterator)); + } + + /** + * Returns deprecations for passed templates. + * + * @param \Traversable $iterator An iterator of templates (where keys are template names and values the contents of the template) + * + * @return array An array of deprecations + */ + public function collect(\Traversable $iterator): array + { + $deprecations = []; + set_error_handler(static function ($type, $msg) use (&$deprecations) { + if (\E_USER_DEPRECATED === $type) { + $deprecations[] = $msg; + } + + return false; + }); + + foreach ($iterator as $name => $contents) { + try { + $this->twig->parse($this->twig->tokenize(new Source($contents, $name))); + } catch (SyntaxError $e) { + // ignore templates containing syntax errors + } + } + + restore_error_handler(); + + return $deprecations; + } +} diff --git a/novaconium/vendor/twig/src/Util/ReflectionCallable.php b/novaconium/vendor/twig/src/Util/ReflectionCallable.php new file mode 100644 index 0000000..9616e3b --- /dev/null +++ b/novaconium/vendor/twig/src/Util/ReflectionCallable.php @@ -0,0 +1,131 @@ + + * + * @internal + */ +final class ReflectionCallable +{ + private $reflector; + private $callable; + private $name; + + public function __construct( + private TwigCallableInterface $twigCallable, + ) { + $callable = $twigCallable->getCallable(); + if (\is_string($callable) && false !== $pos = strpos($callable, '::')) { + $callable = [substr($callable, 0, $pos), substr($callable, 2 + $pos)]; + } + + if (\is_array($callable) && method_exists($callable[0], $callable[1])) { + $this->reflector = $r = new \ReflectionMethod($callable[0], $callable[1]); + $this->callable = $callable; + $this->name = $r->class.'::'.$r->name; + + return; + } + + $checkVisibility = $callable instanceof \Closure; + try { + $closure = \Closure::fromCallable($callable); + } catch (\TypeError $e) { + throw new \LogicException(\sprintf('Callback for %s "%s" is not callable in the current scope.', $twigCallable->getType(), $twigCallable->getName()), 0, $e); + } + $this->reflector = $r = new \ReflectionFunction($closure); + + if (str_contains($r->name, '{closure')) { + $this->callable = $callable; + $this->name = 'Closure'; + + return; + } + + if ($object = $r->getClosureThis()) { + $callable = [$object, $r->name]; + $this->name = get_debug_type($object).'::'.$r->name; + } elseif (\PHP_VERSION_ID >= 80111 && $class = $r->getClosureCalledClass()) { + $callable = [$class->name, $r->name]; + $this->name = $class->name.'::'.$r->name; + } elseif (\PHP_VERSION_ID < 80111 && $class = $r->getClosureScopeClass()) { + $callable = [\is_array($callable) ? $callable[0] : $class->name, $r->name]; + $this->name = (\is_array($callable) ? $callable[0] : $class->name).'::'.$r->name; + } else { + $callable = $this->name = $r->name; + } + + if ($checkVisibility && \is_array($callable) && method_exists(...$callable) && !(new \ReflectionMethod(...$callable))->isPublic()) { + $callable = $r->getClosure(); + } + + $this->callable = $callable; + } + + public function getReflector(): \ReflectionFunctionAbstract + { + return $this->reflector; + } + + /** + * Returns the PHP parameters that map to the callable's template-level + * arguments. + * + * The parameters Twig injects automatically (the piped input value when + * $stripInput is true, then needs_charset/environment/context/is_sandboxed) + * and the bound arguments are stripped. + * + * @return list<\ReflectionParameter> + */ + public function getTwigParameters(bool $stripInput = false): array + { + $parameters = $this->reflector->getParameters(); + if ($stripInput) { + array_shift($parameters); + } + if ($this->twigCallable->needsCharset()) { + array_shift($parameters); + } + if ($this->twigCallable->needsEnvironment()) { + array_shift($parameters); + } + if ($this->twigCallable->needsContext()) { + array_shift($parameters); + } + if (CallExpression::needsIsSandboxed($this->twigCallable)) { + array_shift($parameters); + } + foreach ($this->twigCallable->getArguments() as $argument) { + array_shift($parameters); + } + + return array_values($parameters); + } + + /** + * @return callable + */ + public function getCallable() + { + return $this->callable; + } + + public function getName(): string + { + return $this->name; + } +} diff --git a/novaconium/vendor/twig/src/Util/TemplateDirIterator.php b/novaconium/vendor/twig/src/Util/TemplateDirIterator.php new file mode 100644 index 0000000..d739b28 --- /dev/null +++ b/novaconium/vendor/twig/src/Util/TemplateDirIterator.php @@ -0,0 +1,36 @@ + + */ +class TemplateDirIterator extends \IteratorIterator +{ + /** + * @return string + */ + #[\ReturnTypeWillChange] + public function current() + { + return file_get_contents(parent::current()); + } + + /** + * @return string + */ + #[\ReturnTypeWillChange] + public function key() + { + return (string) parent::key(); + } +} diff --git a/public/.htaccess b/public/.htaccess new file mode 100644 index 0000000..7261584 --- /dev/null +++ b/public/.htaccess @@ -0,0 +1,17 @@ +RewriteEngine On + +# 1) Canonical URLs: strip trailing slash (except bare root) — 301 for SEO. +RewriteCond %{REQUEST_URI} ^/.+/$ +RewriteRule ^(.+)/$ /$1 [R=301,L] + +# 2) Serve pre-rendered static cache file directly, bypassing PHP entirely. +RewriteCond %{DOCUMENT_ROOT}/cache/$1/index.html -f +RewriteRule ^(.*)$ /cache/$1/index.html [L] + +# 3) Serve real files/directories as-is (css, cache assets, etc.). +RewriteCond %{REQUEST_FILENAME} -f [OR] +RewriteCond %{REQUEST_FILENAME} -d +RewriteRule ^ - [L] + +# 4) Everything else goes through the front controller. +RewriteRule ^ index.php [L] diff --git a/public/css/main.css b/public/css/main.css new file mode 100644 index 0000000..cb55de6 --- /dev/null +++ b/public/css/main.css @@ -0,0 +1,433 @@ +:root { + --bg: #14181c; + --surface: #1b2126; + --text-color: #e7ebee; + --muted-color: #98a3ac; + --border-color: #2a3238; + --accent: #2dd4bf; + --accent-hover: #5eead4; +} + +:root[data-theme=light] { + --bg: #ffffff; + --surface: #f1f4f6; + --text-color: #14181c; + --muted-color: #5b6570; + --border-color: #d8dee3; + --accent: #0f9488; + --accent-hover: #0c7a70; +} + +body { + font-family: -apple-system, sans-serif; + background: var(--bg); + color: var(--text-color); + max-width: 40rem; + margin: 2rem auto; + padding: 0 1rem; + line-height: 1.6; +} + +a { + color: var(--accent); + text-decoration: none; +} +a:hover { + color: var(--accent-hover); + text-decoration: underline; +} + +h1, h2, h3, h4, h5, h6 { + color: var(--text-color); + line-height: 1.3; +} + +blockquote { + margin: 1.5rem 0; + padding: 0.25rem 0 0.25rem 1rem; + border-left: 3px solid var(--accent); + color: var(--muted-color); + font-style: italic; +} + +table { + width: 100%; + margin: 1.5rem 0; + border-collapse: collapse; +} + +th, td { + padding: 0.5rem 0.75rem; + border: 1px solid var(--border-color); + text-align: left; +} + +th { + color: var(--text-color); + font-weight: 600; +} + +img { + max-width: 100%; + height: auto; + border-radius: 6px; +} + +.icon { + flex-shrink: 0; +} + +.icon-link { + display: inline-flex; + align-items: center; + gap: 0.35rem; +} + +.icon-heading { + display: inline-flex; + align-items: center; + gap: 0.5rem; +} + +nav { + margin-bottom: 2rem; + padding-bottom: 1rem; + border-bottom: 1px solid var(--border-color); +} +nav a { + display: inline-flex; + align-items: center; + gap: 0.35rem; + margin-right: 1rem; + font-weight: 600; +} + +.theme-toggle { + background: none; + border: none; + color: inherit; + font-weight: normal; + padding: 0; + cursor: pointer; +} +.theme-toggle:hover { + background: none; +} +.theme-toggle .icon-moon { + display: none; +} + +:root[data-theme=light] .theme-toggle .icon-sun { + display: none; +} +:root[data-theme=light] .theme-toggle .icon-moon { + display: inline; +} + +code { + background: var(--surface); + color: var(--accent); + padding: 0.15em 0.4em; + border-radius: 4px; + font-size: 0.9em; +} + +pre { + background: var(--surface); + border: 1px solid var(--border-color); + border-radius: 6px; + padding: 1rem; + overflow-x: auto; +} +pre code { + background: none; + padding: 0; + color: var(--text-color); +} + +hr { + border: none; + border-top: 1px solid var(--border-color); + margin: 2rem 0; +} + +ul, ol { + padding-left: 1.25rem; +} + +label { + color: var(--muted-color); +} + +small { + color: var(--muted-color); +} + +.hp-field { + position: absolute; + left: -9999px; + width: 1px; + height: 1px; + overflow: hidden; +} + +button, select, textarea, input:not([type=radio]):not([type=checkbox]) { + font-family: inherit; + font-size: 1rem; + background: var(--surface); + color: var(--text-color); + border: 1px solid var(--border-color); + border-radius: 6px; + padding: 0.5rem 0.75rem; +} + +select { + cursor: pointer; +} + +input[type=radio], input[type=checkbox] { + accent-color: var(--accent); + margin-right: 0.35rem; + vertical-align: middle; +} + +button { + background: var(--accent); + color: var(--bg); + border: none; + font-weight: 600; + cursor: pointer; +} +button:hover { + background: var(--accent-hover); +} + +.blog-layout { + display: flex; + gap: 2rem; +} +.blog-layout aside { + color: var(--muted-color); + flex: 0 0 8rem; +} +.blog-layout article { + flex: 1; +} + +@keyframes fade-in-up { + from { + opacity: 0; + transform: translateY(0.75rem); + } + to { + opacity: 1; + transform: translateY(0); + } +} +@keyframes glow-drift { + 0% { + transform: translate(-50%, -50%) rotate(0deg); + } + 100% { + transform: translate(-50%, -50%) rotate(360deg); + } +} +.hero { + position: relative; + padding: 3rem 0 2.5rem; + text-align: center; + overflow: hidden; +} +.hero::before { + content: ""; + position: absolute; + top: 50%; + left: 50%; + width: 40rem; + height: 40rem; + background: conic-gradient(from 0deg, transparent 0deg, rgba(45, 212, 191, 0.16) 90deg, transparent 180deg); + animation: glow-drift 18s linear infinite; + pointer-events: none; + z-index: 0; +} +.hero > * { + position: relative; + z-index: 1; + animation: fade-in-up 0.6s ease-out both; +} +.hero h1 { + font-size: 2.5rem; + margin: 0.5rem 0 1rem; + animation-delay: 0.08s; +} +.hero .hero-lede { + animation-delay: 0.16s; +} +.hero .hero-actions { + animation-delay: 0.24s; +} +.hero .hero-badges { + animation-delay: 0.32s; +} + +.hero-eyebrow { + display: inline-block; + color: var(--accent); + font-weight: 700; + letter-spacing: 0.08em; + text-transform: uppercase; + font-size: 0.85rem; +} + +.hero-lede { + max-width: 36rem; + margin: 0 auto; + color: var(--muted-color); + font-size: 1.05rem; +} + +.hero-actions { + display: flex; + justify-content: center; + gap: 1rem; + margin-top: 1.75rem; +} + +.button-link { + display: inline-flex; + align-items: center; + gap: 0.4rem; + background: var(--accent); + color: var(--bg); + font-weight: 600; + border-radius: 6px; + padding: 0.65rem 1.25rem; + text-decoration: none; +} +.button-link:hover { + background: var(--accent-hover); + color: var(--bg); + text-decoration: none; +} +.button-link.button-link--ghost { + background: transparent; + color: var(--text-color); + border: 1px solid var(--border-color); +} +.button-link.button-link--ghost:hover { + background: var(--surface); + color: var(--text-color); +} + +.hero-badges { + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 0.5rem; + margin: 1.5rem 0 0; + padding: 0; + list-style: none; +} + +.badge { + background: var(--surface); + border: 1px solid var(--border-color); + color: var(--muted-color); + border-radius: 999px; + padding: 0.3rem 0.85rem; + font-size: 0.8rem; + font-weight: 600; +} + +.feature-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(15rem, 1fr)); + gap: 1.25rem; + margin: 2.5rem 0; +} + +.feature-card { + background: var(--surface); + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 1.25rem 1.5rem; + opacity: 0; + animation: fade-in-up 0.5s ease-out forwards; +} +.feature-card:nth-child(1) { + animation-delay: 0.36s; +} +.feature-card:nth-child(2) { + animation-delay: 0.42s; +} +.feature-card:nth-child(3) { + animation-delay: 0.48s; +} +.feature-card:nth-child(4) { + animation-delay: 0.54s; +} +.feature-card:nth-child(5) { + animation-delay: 0.6s; +} +.feature-card:nth-child(6) { + animation-delay: 0.66s; +} +.feature-card h2 { + font-size: 1.1rem; + margin: 0 0 0.5rem; +} +.feature-card p { + color: var(--muted-color); + margin: 0; + font-size: 0.95rem; +} + +@media (prefers-reduced-motion: reduce) { + .hero::before { + animation: none; + } + .hero > *, .feature-card { + animation: none; + opacity: 1; + transform: none; + } +} +.next-steps { + border-top: 1px solid var(--border-color); + padding-top: 2rem; + margin-top: 1rem; +} +.next-steps ul { + padding-left: 1.25rem; +} + +.post-list { + list-style: none; + padding: 0; +} +.post-list li { + padding: 1rem 0; + border-bottom: 1px solid var(--border-color); +} +.post-list li:last-child { + border-bottom: none; +} +.post-list h2 { + margin: 0 0 0.35rem; + font-size: 1.15rem; +} +.post-list p { + color: var(--muted-color); + margin: 0; +} + +.footnotes { + border-top: 1px solid var(--border-color); + margin-top: 2.5rem; + padding-top: 1rem; + font-size: 0.9rem; + color: var(--muted-color); +} +.footnotes ol { + padding-left: 1.25rem; +} diff --git a/public/index.php b/public/index.php new file mode 100644 index 0000000..e2e847d --- /dev/null +++ b/public/index.php @@ -0,0 +1,3 @@ +div - font-size: 0.8rem - margin: 8px 0 0 20px \ No newline at end of file diff --git a/sass/framework/_forms.sass b/sass/framework/_forms.sass deleted file mode 100644 index 8cb500f..0000000 --- a/sass/framework/_forms.sass +++ /dev/null @@ -1,62 +0,0 @@ -// pages/_forms.sass - -@use '../abstracts' as * -@use '../base' as * - -body.novaconium - #edit-page-form-novaconium - // Form groups: Spacing - .form-group - margin-bottom: space('md') // 1rem - - &.fullwidth textarea - width: 100% - resize: vertical - - .checkbox-group - margin-top: space('lg') // 1.5rem - - // Inputs: Dark bg, green focus - input[type="text"], - input[type="number"], - textarea, - select - background-color: color('bg-darker') // Deeper green-black - border: 1px solid color('border-light') - color: $text-light - padding: space('sm') - border-radius: 4px - font-family: 'VT323', $mono-font // Matrix monospace - - &:focus - border-color: $accent-light // Lime focus - box-shadow: 0 0 0 space('xs') rgba($accent-light, 0.3) // Glow ring - outline: none - - input[type="checkbox"] - accent-color: $accent-light // Green checkbox - - // Submit button: Green primary - button[type="submit"] - background-color: $accent-light - color: color('bg-dark') // Dark text on green - border: none - padding: space('sm') space('md') - border-radius: 4px - cursor: pointer - font-family: 'VT323', $mono-font - - &:hover, &:focus - background-color: hsl(120, 100%, 30%) // Darker green (35% L -5%) - box-shadow: 0 0 4px rgba($accent-light, 0.5) - - // Text/links: Muted with hover - p - color: $text-muted - - a - color: $accent-light - - &:hover - color: $text-lighter // Brighter green - text-decoration: underline \ No newline at end of file diff --git a/sass/framework/_login_form.sass b/sass/framework/_login_form.sass deleted file mode 100644 index b88c8b4..0000000 --- a/sass/framework/_login_form.sass +++ /dev/null @@ -1,53 +0,0 @@ -// framework/_login_form.sass - -@use '../abstracts' as * - -body.novaconium #login - // No background or border—use parent's ambient styling - padding: 0 // Or keep minimal if needed; adjust to 2rem if you want internal space - // No centering: left-aligned by default - - // Wrapper for inputs/button to align widths - input[type="text"], - input[type="password"], - button[type="submit"] - width: 100% // Full width of form (no max cap for flow) - max-width: 300px // Keep cap for consistency across fields - padding: 1rem - margin-bottom: 1rem - box-sizing: border-box // Include padding in width - font-size: 1rem - border: 1px solid $border-light - border-radius: 4px - background: $bg-dark - color: $text-light - - // Icon backgrounds: Cyber icons via data URI, now in white for visibility - input[type="text"] // Username input - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2'/%3E%3Ccircle cx='12' cy='7' r='4'/%3E%3C/svg%3E") - background-repeat: no-repeat - background-position: 1rem center - background-size: 20px - padding-left: 3rem // Space for icon - - input[type="password"] // Password input - background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='11' width='18' height='11' rx='2' ry='2'/%3E%3Cpath d='M7 11V7a5 5 0 0 1 10 0v4'/%3E%3C/svg%3E") - background-repeat: no-repeat - background-position: 1rem center - background-size: 20px - padding-left: 3rem // Space for icon - - button[type="submit"] - background: $accent-light // Green accent for submit - border-color: $accent-light - cursor: pointer - transition: background 0.2s - - &:hover - background: $accent-light - text-shadow: 0 0 5px rgba($accent-light, 0.5) // Cyber glow - - // No icon needed for button, but add if you want (e.g., arrow) - &::after - content: ' →' // Simple arrow, or swap for Material Icon - margin-left: 0.5rem \ No newline at end of file diff --git a/sass/framework/_logo.sass b/sass/framework/_logo.sass deleted file mode 100644 index e112e60..0000000 --- a/sass/framework/_logo.sass +++ /dev/null @@ -1,79 +0,0 @@ -body.novaconium - #biglogo - position: relative - display: inline-block - padding: 20px 0 - border-radius: 0 // Keep it sharp, no curves - overflow: hidden - - &::before - content: '' - position: absolute - top: 0 - left: 0 - right: 0 - bottom: 0 - background-image: linear-gradient(rgba(0, 255, 0, 0.02) 1px, transparent 1px), linear-gradient(90deg, rgba(0, 255, 0, 0.02) 1px, transparent 1px) - background-size: 20px 20px - opacity: 0.1 // Subtle grid/matrix rain hint - pointer-events: none - - // Scan lines for extra flicker - &::after - content: '' - position: absolute - top: 0 - left: 0 - right: 0 - height: 1px - background: linear-gradient(90deg, transparent, #00ff00, transparent) - opacity: 0.05 - animation: scan 3s linear infinite - - .main - display: block - font-family: 'Orbitron', sans-serif // Cyberpunk geometric punch - font-size: 4.5rem // Chunky figlet scale - font-weight: 900 // Black weight for max angular depth - color: #fff - letter-spacing: 0.1em // Slightly looser for Orbitron's geometry - line-height: 0.85 // Compact height like figlet blocks - text-transform: uppercase - - // Multi-layer shadow to mimic ansishadow's depth: base shadow + edge glow - text-shadow: 4px 4px 0 #222, 5px 5px 0 #111, -1px -1px 0 #00ff00, 1px 1px 20px rgba(0, 255, 0, 0.3) // Main drop shadow, deeper offset, subtle green edge highlight for cyber pop, glow for security flair - filter: drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.8)) // Browser shadow boost - - .sub - display: block - font-family: 'Orbitron', sans-serif // Match for cohesion, or swap to monospace if you want contrast - font-size: 2.5rem - font-weight: 700 // Bold but not overpowering - color: #fff - letter-spacing: 0.15em // Wider for emphasis - margin-top: 0.5rem - text-shadow: 2px 2px 0 #222, -1px -1px 0 #00ff00, 1px 1px 10px rgba(0, 255, 0, 0.2) // Lighter shadow, subtle green edge, glow - filter: drop-shadow(1px 1px 2px rgba(0, 0, 0, 0.6)) - - // Optional: Glitch animation on hover for that hacked feel - &:hover - .main - animation: glitch 0.3s infinite - - @keyframes scan - 0% - top: -1px - 100% - top: 100% - - @keyframes glitch - 0%, 100% - transform: translate(0) - 20% - transform: translate(-2px, 2px) - 40% - transform: translate(-2px, -2px) - 60% - transform: translate(2px, 2px) - 80% - transform: translate(2px, -2px) \ No newline at end of file diff --git a/sass/framework/_main.sass b/sass/framework/_main.sass deleted file mode 100644 index 1dcdebb..0000000 --- a/sass/framework/_main.sass +++ /dev/null @@ -1,62 +0,0 @@ -// layout/_main.sass - -@use '../abstracts' as * -@use '../base' as * - -body.novaconium - // Main page wrapper: Centered flex - #page .container - display: flex - padding: 0 - justify-content: center - align-items: flex-start - padding-top: space('lg') - - // Article content: Fixed width, no shrink - article - width: 1140px - flex-shrink: 0 - padding: 3rem - margin: 0 - background-color: $bg-dark - border: 1px solid $bg-darker - - ul#leftnav - width: 320px - flex-shrink: 0 - list-style: none - padding: 0 - margin: 0 space('xl') 0 0 // Right margin preserved (overrides the 0 for other sides) - background-color: $bg-dark - border: 1px solid $bg-darker - - #leftnav li - border-bottom: 1px solid color('border-light') - - &:last-child - border-bottom: none - - #leftnav a - display: block - padding: space('sm') space('md') // Consistent spacing - text-decoration: none - color: $text-light // Theme text - - &:hover, &:focus - background-color: rgba($accent-light, 0.1) // Green hover tint - color: $accent-light // Accent on hover - border-radius: 4px - - // Responsive: Stack on mobile (add to themes/ for media queries) - @media (max-width: 1280px) // Your xl breakpoint - #page .container - flex-direction: column - align-items: center - - article, #leftnav - width: 100% - max-width: 900px - margin: space('md') 0 - - #leftnav - margin-right: 0 \ No newline at end of file diff --git a/sass/framework/_tabs.sass b/sass/framework/_tabs.sass deleted file mode 100644 index 7af4198..0000000 --- a/sass/framework/_tabs.sass +++ /dev/null @@ -1,49 +0,0 @@ -// framework/_tabs.sass - -@use '../abstracts' as * -@use 'sass:color' // For color.adjust()—non-deprecated color tweaks - -body#controlPanel - // Simplified tab styling: Square borders, no rounds - .tab-container - margin: space('lg') auto - background: $bg-dark - - .tab-nav - display: flex - background-color: $bg-darker - border-bottom: 1px solid $border-light - - .tab-button - padding: space('sm') space('md') - background: $bg-darker - border: 1px solid $border-light - border-bottom: none - cursor: pointer - font-size: 14px - min-width: 120px // Makes them more square/equal - text-align: center - color: $text-light - font-family: $mono-font // From vars - - &:hover - background-color: color.adjust($bg-darker, $lightness: 5%) // Modern, non-deprecated lighten equivalent - - + .tab-button - border-left: none // Shared borders - - &.active - background-color: $bg-dark - border-bottom: 2px solid $accent-light - color: $accent-light - font-weight: bold - - .tab-content - display: none - padding: space('lg') - background-color: $bg-dark - color: $text-light - border: none - - &.active - display: block diff --git a/sass/framework/_tags.sass b/sass/framework/_tags.sass deleted file mode 100644 index 6d458e7..0000000 --- a/sass/framework/_tags.sass +++ /dev/null @@ -1,109 +0,0 @@ -@use '../abstracts' as * -@use 'sass:color' // For color.adjust()—non-deprecated color tweaks -@use '../base' as * - -body.novaconium - // Tags Dropdown - .tags-dropdown - position: absolute - top: 100% - left: 0 - right: 0 - max-height: 200px - overflow-y: auto - background: $bg-darker - border: 1px solid $border-light - border-top: none - border-radius: 0 0 4px 4px - list-style: none - margin: 0 - padding: 0 - z-index: 10 - opacity: 0 - visibility: hidden - transition: opacity 0.2s - - &[aria-expanded="true"] - opacity: 1 - visibility: visible - - li - padding: space('xs') space('sm') - cursor: pointer - color: $text-light - font-size: 14px - border-bottom: 1px solid transparent - - &:hover, - &.selected - background: color.adjust($accent-light, $alpha: -0.1) - color: $accent-light - - &:last-child - border-bottom: none - - .tags-input - position: relative // For absolute dropdown - - // Tags Input - .tags-input - display: flex - flex-wrap: wrap - gap: space('xs') - align-items: center - border: 1px solid $border-light - padding: space('xs') - background: $bg-darker - min-height: 40px - grid-column: 2 // Spans input area in .form-row grid - - input - border: none - background: transparent - color: $text-light - font-family: $mono-font - font-size: 14px - flex: 1 - min-width: 100px - outline: none - - &::placeholder - color: $text-muted - - .tag-chip - display: inline-flex - align-items: center - background: color.adjust($accent-light, $alpha: -0.2) - color: #fff - padding: space('xs') space('sm') - border-radius: 4px - font-size: 12px - max-width: 150px - overflow: hidden - text-overflow: ellipsis - white-space: nowrap - - .tag-remove - background: none - border: none - color: inherit - font-size: 16px - margin-left: space('xs') - cursor: pointer - padding: 0 - line-height: 1 - - &:hover - opacity: 0.7 - - // Tags Autocomplete (native datalist styling) - .tags-input input[list] - // Existing input styles apply... - - #tag-suggestions - // Native datalist not directly stylable, but options can be influenced via input focus - // For custom polyfill, add JS-generated
      , but native is fine for now - - // Browser-specific tweaks (Chrome/Firefox) - .tags-input input[list]::-webkit-calendar-picker-indicator - display: none // Hide arrow if interfering \ No newline at end of file diff --git a/sass/framework/_tooltip.sass b/sass/framework/_tooltip.sass deleted file mode 100644 index 80a8f05..0000000 --- a/sass/framework/_tooltip.sass +++ /dev/null @@ -1,48 +0,0 @@ -@use '../abstracts' as * -@use '../base' as * -@use 'sass:color' // Already there for adjusts - -body.novaconium - // Tooltip styling (mouse-follow version) - .tooltip - position: relative - display: inline-block - cursor: help - font-size: 16px - color: $accent-light // Green accent for ? - margin-left: space('xs') // 0.25rem - top: 2px - - .tooltiptext - visibility: hidden - width: 200px - background-color: $bg-darker // Darker bg - color: $text-light // White text - text-align: left // Changed to left-align for better readability - border-radius: 0 // Square - padding: space('sm') // 0.5rem - position: fixed // Fixed to viewport, updated by JS - z-index: z('dropdown') // High z-index - opacity: 0 - transition: opacity 0.3s - font-size: 14px - box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2) // Subtle shadow - pointer-events: none // Prevents mouse interference with tooltip - - &::after - content: "" - position: absolute - top: -5px // Arrow above tooltip (points up to mouse) - left: 10px // Slight indent from left edge - border-width: 5px - border-style: solid - border-color: $bg-darker transparent transparent transparent // Upward arrow - - &:hover .tooltiptext - visibility: visible - opacity: 1 - - // Accessibility: Hidden tooltip text for screen readers - .tooltip-desc - position: absolute - left: -9999px \ No newline at end of file diff --git a/sass/framework/_ui.sass b/sass/framework/_ui.sass deleted file mode 100644 index 32d35bc..0000000 --- a/sass/framework/_ui.sass +++ /dev/null @@ -1,59 +0,0 @@ -// components/_ui.sass - -@use '../abstracts' as * -@use '../base' as * - -body.novaconium - // Code blocks: Monospace lock-in with darker green tint - code - font-family: 'VT323', $mono-font // Prioritize Matrix font - font-size: 0.875rem // Smaller for inline; reset has base - background-color: color('bg-darker') // Deeper green-black - color: $text-light // Lighter green text - padding: space('xs') space('sm') - border-radius: 4px - border: 1px solid color('border-light') - box-shadow: 0 0 2px rgba($accent-light, 0.1) // Subtle glow - - // Utility: Small text (if still needed; consider rem-based) - .small - font-size: 0.625rem // 10px equiv; use sparingly - - // Error/notice divs: Centered alerts with theme colors - div.error, div.notice, div#debug - margin: space('xl') auto // Top/bottom spacing from map - width: 900px // Fixed width; add media query for mobile later - padding: space('lg') // Generous padding - border-radius: 6px - border: 1px solid - - div.error - background-color: rgba(color('accent-error'), 0.1) // Subtle red tint - color: color('accent-error') // Darker red text - border-color: color('accent-error') - - div.notice - background-color: rgba(color('accent-success'), 0.1) // Green tint - color: color('accent-success') // Heavier green text - border-color: color('accent-success') - - div#debug - background-color: color('bg-darker') // Deeper bg for debug - color: $text-muted // Muted green - border-color: color('border-light') - margin-top: space('2xl') - margin-bottom: space('2xl') - - // Tables: Simplified; reset handles collapse/padding - .pages-table - width: 100% - border: 1px solid color('border-light') // Green border - - th, td - border: 1px solid color('border-light') - text-align: left - - th - background-color: rgba($accent-light, 0.05) // Subtle accent bg - color: $text-lighter // Header green - font-weight: 600 // Semi-bold; no bold in monospace \ No newline at end of file diff --git a/sass/framework/index.sass b/sass/framework/index.sass deleted file mode 100644 index c691b6a..0000000 --- a/sass/framework/index.sass +++ /dev/null @@ -1,10 +0,0 @@ -@forward 'main'; -@forward 'ui'; -@forward 'forms'; -@forward 'login_form'; -@forward 'logo'; -@forward 'tabs'; -@forward 'edit_page'; -@forward 'tooltip'; -@forward 'ace'; -@forward 'tags'; \ No newline at end of file diff --git a/sass/novaconium.sass b/sass/novaconium.sass deleted file mode 100644 index 04bb0d4..0000000 --- a/sass/novaconium.sass +++ /dev/null @@ -1,6 +0,0 @@ -// novaconium.sass -@use 'abstracts' as * -@use 'base' as * -@use 'framework' as * -@use 'controlPanel' as * -@use 'coming-soon' as * \ No newline at end of file diff --git a/skeleton/.env b/skeleton/.env deleted file mode 100644 index 8eec68a..0000000 --- a/skeleton/.env +++ /dev/null @@ -1,2 +0,0 @@ -MYSQL_ROOT_PASSWORD=random -MYSQL_PASSWORD=random diff --git a/skeleton/.gitignore b/skeleton/.gitignore deleted file mode 100644 index b20ce52..0000000 --- a/skeleton/.gitignore +++ /dev/null @@ -1,3 +0,0 @@ -data/ -novaconium/vendor/ -novaconium/logs/ \ No newline at end of file diff --git a/skeleton/docker-compose.yml b/skeleton/docker-compose.yml deleted file mode 100644 index db2402b..0000000 --- a/skeleton/docker-compose.yml +++ /dev/null @@ -1,53 +0,0 @@ -# Sample Docker Compose -services: - corxn: - image: 4lights/corxn:8.5.3 - ports: - - "8000:80" - volumes: - - ./novaconium:/data - - ./data/logs:/var/log/apache2 # Optional Logs - - "./logs:/data/logs" - restart: unless-stopped - networks: - - internal - - proxy - - redis: - image: redis:latest - networks: - - internal - restart: unless-stopped - - mariadb: - image: mariadb:latest - environment: - MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD} - MYSQL_DATABASE: novadb - MYSQL_USER: novaconium - MYSQL_PASSWORD: ${MYSQL_PASSWORD} - volumes: - - ./data/db:/var/lib/mysql - networks: - - internal - restart: unless-stopped - - phpmyadmin: - image: phpmyadmin/phpmyadmin:latest - restart: unless-stopped - ports: - - "8001:80" - networks: - - internal - environment: - - PMA_ARBITRARY=-1 - - PMA_HOST=mariadb - - PMA_USER=root - - PMA_PASSWORD=${MYSQL_ROOT_PASSWORD} - - UPLOAD_LIMIT=200M - -networks: - proxy: - external: true - internal: - driver: bridge diff --git a/skeleton/novaconium/App/config.php b/skeleton/novaconium/App/config.php deleted file mode 100644 index e6e653f..0000000 --- a/skeleton/novaconium/App/config.php +++ /dev/null @@ -1,17 +0,0 @@ - [ - 'host' => 'mariadb', - 'name' => 'novadb', - 'user' => 'novaconium', - 'pass' => '', - 'port' => 3306 - ], - 'base_url' => 'http://localhost:8000', - 'secure_key' => '', //64 alphanumeric characters - 'logfile' => '/logs/novaconium.log', - 'loglevel' => 'ERROR', // 'DEBUG', 'INFO', 'WARNING', 'ERROR', 'NONE', - 'matomo_url' => 'matomo.4lt.ca', - 'matomo_id' => '0', - 'fonts' => 'https://fonts.googleapis.com/css2?family=VT323:wght@400&family=Fira+Code:wght@400;500&display=swap&family=Material+Icons:wght@400;500&display=swap' -]; diff --git a/skeleton/novaconium/App/controllers/humans.php b/skeleton/novaconium/App/controllers/humans.php deleted file mode 100644 index ab127b7..0000000 --- a/skeleton/novaconium/App/controllers/humans.php +++ /dev/null @@ -1,28 +0,0 @@ - 'Welcome to Novaconium Index Page', - 'pageclass' => 'novaconium' -]); - -view('index'); diff --git a/skeleton/novaconium/App/controllers/page.php b/skeleton/novaconium/App/controllers/page.php deleted file mode 100644 index 70a40b7..0000000 --- a/skeleton/novaconium/App/controllers/page.php +++ /dev/null @@ -1,33 +0,0 @@ -parameters['slug']; -$query=<<debugGetRow($query); -$data = $db->getRow($query); -if(!$data) { - http_response_code('404'); - header("Content-Type: text/html"); - view('404'); - exit; -} - -$data = array_merge($data, [ - 'menuActive' => 'blog', - 'pageid' => 'page', - 'permissionsGroup' => $session->get('group') -]); - -view('page', $data); diff --git a/skeleton/novaconium/App/controllers/robots.php b/skeleton/novaconium/App/controllers/robots.php deleted file mode 100644 index 115305d..0000000 --- a/skeleton/novaconium/App/controllers/robots.php +++ /dev/null @@ -1,18 +0,0 @@ - [ - 'get' => 'index' - ], - '/robots.txt' => [ - 'get' => 'robots' - ], - '/humans.txt' => [ - 'get' => 'humans' - ], - '/page/{slug}' => [ - 'get' => 'page' - ] -]; \ No newline at end of file diff --git a/skeleton/novaconium/App/templates/override.html.twig b/skeleton/novaconium/App/templates/override.html.twig deleted file mode 100644 index edd7fd0..0000000 --- a/skeleton/novaconium/App/templates/override.html.twig +++ /dev/null @@ -1 +0,0 @@ -{# Overrides go here #} \ No newline at end of file diff --git a/skeleton/novaconium/App/views/index.html.twig b/skeleton/novaconium/App/views/index.html.twig deleted file mode 100644 index 5b2293d..0000000 --- a/skeleton/novaconium/App/views/index.html.twig +++ /dev/null @@ -1,21 +0,0 @@ -{% extends '@novaconium/master.html.twig' %} - -{% block content %} -

      Novaconium PHP

      -

      Minimalist PHP framework

      -

      - Edit App/routes.php and App/controllers/index.php
      - to customize this page. -

      -

      Sign in to the Administration

      - -

      Documentation

      - - -

      Repository

      -

      Visit Source Control Repository for Novaconium

      - -{% endblock %} diff --git a/skeleton/novaconium/App/views/page.html.twig b/skeleton/novaconium/App/views/page.html.twig deleted file mode 100644 index 6c0ff09..0000000 --- a/skeleton/novaconium/App/views/page.html.twig +++ /dev/null @@ -1,11 +0,0 @@ -{% extends '@novaconium/master.html.twig' %} - -{% block content %} -

      {{ title }}

      -
      - {% if updated is not empty %} - Last Updated: {{ updated | date("M\\. jS Y \\a\\t g:ia") }} - {% endif %} -
      - {{ body | raw}} -{% endblock %} diff --git a/skeleton/novaconium/public/.htaccess b/skeleton/novaconium/public/.htaccess deleted file mode 100644 index 13eafff..0000000 --- a/skeleton/novaconium/public/.htaccess +++ /dev/null @@ -1,4 +0,0 @@ -RewriteEngine On -RewriteCond %{REQUEST_FILENAME} !-f -RewriteCond %{REQUEST_FILENAME} !-d -RewriteRule ^(.*)$ index.php?_uri=$1 [QSA,L] diff --git a/skeleton/novaconium/public/css/novaconium.css b/skeleton/novaconium/public/css/novaconium.css deleted file mode 100644 index 08c5b1d..0000000 --- a/skeleton/novaconium/public/css/novaconium.css +++ /dev/null @@ -1 +0,0 @@ -:root{--bg-dark: hsl(0, 0%, 2%);--bg-darker: hsl(210, 7%, 5%);--text-light: hsl(0, 0%, 100%);--text-lighter: hsl(120, 52%, 15%);--text-muted: hsl(120, 40%, 45%);--accent-light: hsl(120, 100%, 35%);--accent-success: hsl(120, 80%, 45%);--accent-warning: hsl(60, 100%, 35%);--accent-error: hsl(0, 100%, 45%);--border-light: hsla(120, 60%, 70%, 0.2);--font-stack: Courier New, SF Mono, monospace}*{box-sizing:border-box}html{font-size:16px;line-height:1.5;scroll-behavior:smooth;background-color:hsl(0,0%,2%);color:#fff}body{margin:0;padding:0;font-family:"Courier New","SF Mono",monospace;background-color:hsl(0,0%,2%);color:#fff;min-height:100vh}h1,h2,h3,h4,h5,h6{margin:0 0 .5em;font-weight:600;line-height:1.2;color:rgb(0,178.5,0)}h1{font-size:2.5rem}h2{font-size:2rem}h3{font-size:1.75rem}h4{font-size:1.5rem}h5{font-size:1.25rem}h6{font-size:1rem}p{margin:0 0 1em}small{font-size:.875rem}strong,b{font-weight:700}em,i{font-style:italic}a{color:rgb(0,178.5,0);text-decoration:none;border-bottom:1px solid rgba(0,0,0,0);transition:border-color .2s ease}a:hover,a:focus{border-bottom-color:rgb(0,178.5,0);outline:none}ul,ol{margin:0 0 1em;padding-left:1.5em}li{margin-bottom:.25em}blockquote{margin:1em 0;padding:.5em 1em;border-left:4px solid rgb(0,178.5,0);background-color:hsla(0,0%,2%,.5);color:#fff}code,kbd,samp{font-family:"Courier New","SF Mono","Fira Code",Consolas,monospace;font-size:.875em;background-color:hsla(0,0%,100%,.1);padding:.125em .25em;border-radius:3px}pre{margin:1em 0;padding:1em;overflow:auto;background-color:hsla(0,0%,100%,.1);border-radius:4px}pre code{background:none;padding:0}table{width:100%;border-collapse:collapse;margin:1em 0}th,td{padding:.75em;text-align:left;border-bottom:1px solid hsla(0,0%,100%,.2)}th{font-weight:600}input,select,textarea,button{font-family:inherit;font-size:inherit;background-color:hsla(0,0%,100%,.1);color:#fff;border:1px solid hsla(0,0%,100%,.3);border-radius:4px;padding:.5em}input:focus,select:focus,textarea:focus,button:focus{outline:none;border-color:rgb(0,178.5,0);box-shadow:0 0 0 2px rgba(0,178.5,0,.2)}button{cursor:pointer;transition:background-color .2s ease}button:disabled{opacity:.5;cursor:not-allowed}img{max-width:100%;height:auto;border-radius:4px}figure{margin:1em 0}hr{border:none;height:1px;background-color:hsla(0,0%,100%,.2);margin:2em 0}*:focus-visible{outline:2px solid rgb(0,178.5,0);outline-offset:2px}@media print{body{background:#fff !important;color:#000 !important}}::selection{background:rgba(0,178.5,0,.3);color:#fff}*::-moz-selection{background:rgba(0,178.5,0,.3);color:#fff}body.novaconium{background-color:#000;background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 304 304' width='304' height='304'%3E%3Cpath fill='%231f2c17' fill-opacity='0.4' d='M44.1 224a5 5 0 1 1 0 2H0v-2h44.1zm160 48a5 5 0 1 1 0 2H82v-2h122.1zm57.8-46a5 5 0 1 1 0-2H304v2h-42.1zm0 16a5 5 0 1 1 0-2H304v2h-42.1zm6.2-114a5 5 0 1 1 0 2h-86.2a5 5 0 1 1 0-2h86.2zm-256-48a5 5 0 1 1 0 2H0v-2h12.1zm185.8 34a5 5 0 1 1 0-2h86.2a5 5 0 1 1 0 2h-86.2zM258 12.1a5 5 0 1 1-2 0V0h2v12.1zm-64 208a5 5 0 1 1-2 0v-54.2a5 5 0 1 1 2 0v54.2zm48-198.2V80h62v2h-64V21.9a5 5 0 1 1 2 0zm16 16V64h46v2h-48V37.9a5 5 0 1 1 2 0zm-128 96V208h16v12.1a5 5 0 1 1-2 0V210h-16v-76.1a5 5 0 1 1 2 0zm-5.9-21.9a5 5 0 1 1 0 2H114v48H85.9a5 5 0 1 1 0-2H112v-48h12.1zm-6.2 130a5 5 0 1 1 0-2H176v-74.1a5 5 0 1 1 2 0V242h-60.1zm-16-64a5 5 0 1 1 0-2H114v48h10.1a5 5 0 1 1 0 2H112v-48h-10.1zM66 284.1a5 5 0 1 1-2 0V274H50v30h-2v-32h18v12.1zM236.1 176a5 5 0 1 1 0 2H226v94h48v32h-2v-30h-48v-98h12.1zm25.8-30a5 5 0 1 1 0-2H274v44.1a5 5 0 1 1-2 0V146h-10.1zm-64 96a5 5 0 1 1 0-2H208v-80h16v-14h-42.1a5 5 0 1 1 0-2H226v18h-16v80h-12.1zm86.2-210a5 5 0 1 1 0 2H272V0h2v32h10.1zM98 101.9V146H53.9a5 5 0 1 1 0-2H96v-42.1a5 5 0 1 1 2 0zM53.9 34a5 5 0 1 1 0-2H80V0h2v34H53.9zm60.1 3.9V66H82v64H69.9a5 5 0 1 1 0-2H80V64h32V37.9a5 5 0 1 1 2 0zM101.9 82a5 5 0 1 1 0-2H128V37.9a5 5 0 1 1 2 0V82h-28.1zm16-64a5 5 0 1 1 0-2H146v44.1a5 5 0 1 1-2 0V18h-26.1zm102.2 270a5 5 0 1 1 0 2H98v14h-2v-16h124.1zM242 149.9V160h16v34h-16v62h48v48h-2v-46h-48v-66h16v-30h-16v-12.1a5 5 0 1 1 2 0zM53.9 18a5 5 0 1 1 0-2H64V2H48V0h18v18H53.9zm112 32a5 5 0 1 1 0-2H192V0h50v2h-48v48h-28.1zm-48-48a5 5 0 0 1-9.8-2h2.07a3 3 0 1 0 5.66 0H178v34h-18V21.9a5 5 0 1 1 2 0V32h14V2h-58.1zm0 96a5 5 0 1 1 0-2H137l32-32h39V21.9a5 5 0 1 1 2 0V66h-40.17l-32 32H117.9zm28.1 90.1a5 5 0 1 1-2 0v-76.51L175.59 80H224V21.9a5 5 0 1 1 2 0V82h-49.59L146 112.41v75.69zm16 32a5 5 0 1 1-2 0v-99.51L184.59 96H300.1a5 5 0 0 1 3.9-3.9v2.07a3 3 0 0 0 0 5.66v2.07a5 5 0 0 1-3.9-3.9H185.41L162 121.41v98.69zm-144-64a5 5 0 1 1-2 0v-3.51l48-48V48h32V0h2v50H66v55.41l-48 48v2.69zM50 53.9v43.51l-48 48V208h26.1a5 5 0 1 1 0 2H0v-65.41l48-48V53.9a5 5 0 1 1 2 0zm-16 16V89.41l-34 34v-2.82l32-32V69.9a5 5 0 1 1 2 0zM12.1 32a5 5 0 1 1 0 2H9.41L0 43.41V40.6L8.59 32h3.51zm265.8 18a5 5 0 1 1 0-2h18.69l7.41-7.41v2.82L297.41 50H277.9zm-16 160a5 5 0 1 1 0-2H288v-71.41l16-16v2.82l-14 14V210h-28.1zm-208 32a5 5 0 1 1 0-2H64v-22.59L40.59 194H21.9a5 5 0 1 1 0-2H41.41L66 216.59V242H53.9zm150.2 14a5 5 0 1 1 0 2H96v-56.6L56.6 162H37.9a5 5 0 1 1 0-2h19.5L98 200.6V256h106.1zm-150.2 2a5 5 0 1 1 0-2H80v-46.59L48.59 178H21.9a5 5 0 1 1 0-2H49.41L82 208.59V258H53.9zM34 39.8v1.61L9.41 66H0v-2h8.59L32 40.59V0h2v39.8zM2 300.1a5 5 0 0 1 3.9 3.9H3.83A3 3 0 0 0 0 302.17V256h18v48h-2v-46H2v42.1zM34 241v63h-2v-62H0v-2h34v1zM17 18H0v-2h16V0h2v18h-1zm273-2h14v2h-16V0h2v16zm-32 273v15h-2v-14h-14v14h-2v-16h18v1zM0 92.1A5.02 5.02 0 0 1 6 97a5 5 0 0 1-6 4.9v-2.07a3 3 0 1 0 0-5.66V92.1zM80 272h2v32h-2v-32zm37.9 32h-2.07a3 3 0 0 0-5.66 0h-2.07a5 5 0 0 1 9.8 0zM5.9 0A5.02 5.02 0 0 1 0 5.9V3.83A3 3 0 0 0 3.83 0H5.9zm294.2 0h2.07A3 3 0 0 0 304 3.83V5.9a5 5 0 0 1-3.9-5.9zm3.9 300.1v2.07a3 3 0 0 0-1.83 1.83h-2.07a5 5 0 0 1 3.9-3.9zM97 100a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0-16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-48 32a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm32 48a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-16 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm32-16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0-32a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16 32a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm32 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0-16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-16-64a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16 0a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16 96a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16-144a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0 32a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16-32a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16-16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-96 0a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16-32a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm96 0a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-16-64a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16-16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-32 0a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0-16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-16 0a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-16 0a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-16 0a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM49 36a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-32 0a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm32 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM33 68a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16-48a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0 240a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16 32a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-16-64a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-16-32a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm80-176a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16 0a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-16-16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm32 48a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16-16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0-32a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm112 176a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm-16 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM17 180a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0 16a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm0-32a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16 0a3 3 0 1 0 0-6 3 3 0 0 0 0 6zM17 84a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm32 64a3 3 0 1 0 0-6 3 3 0 0 0 0 6zm16-16a3 3 0 1 0 0-6 3 3 0 0 0 0 6z'%3E%3C/path%3E%3C/svg%3E")}body.novaconium #page .container{display:flex;padding:0;justify-content:center;align-items:flex-start;padding-top:1.5rem}body.novaconium article{width:1140px;flex-shrink:0;padding:3rem;margin:0;background-color:hsl(0,0%,2%);border:1px solid hsl(210,7%,5%)}body.novaconium ul#leftnav{width:320px;flex-shrink:0;list-style:none;padding:0;margin:0 2.5rem 0 0;background-color:hsl(0,0%,2%);border:1px solid hsl(210,7%,5%)}body.novaconium #leftnav li{border-bottom:1px solid hsla(120,60%,70%,.2)}body.novaconium #leftnav li:last-child{border-bottom:none}body.novaconium #leftnav a{display:block;padding:.5rem 1rem;text-decoration:none;color:#fff}body.novaconium #leftnav a:hover,body.novaconium #leftnav a:focus{background-color:rgba(0,178.5,0,.1);color:rgb(0,178.5,0);border-radius:4px}@media(max-width: 1280px){body.novaconium #page .container{flex-direction:column;align-items:center}body.novaconium article,body.novaconium #leftnav{width:100%;max-width:900px;margin:1rem 0}body.novaconium #leftnav{margin-right:0}}body.novaconium code{font-family:"VT323","Courier New","SF Mono","Fira Code",Consolas,monospace;font-size:.875rem;background-color:hsl(210,7%,5%);color:#fff;padding:.25rem .5rem;border-radius:4px;border:1px solid hsla(120,60%,70%,.2);box-shadow:0 0 2px rgba(0,178.5,0,.1)}body.novaconium .small{font-size:.625rem}body.novaconium div.error,body.novaconium div.notice,body.novaconium div#debug{margin:2.5rem auto;width:900px;padding:1.5rem;border-radius:6px;border:1px solid}body.novaconium div.error{background-color:rgba(229.5,0,0,.1);color:rgb(229.5,0,0);border-color:rgb(229.5,0,0)}body.novaconium div.notice{background-color:hsla(120,80%,45%,.1);color:hsl(120,80%,45%);border-color:hsl(120,80%,45%)}body.novaconium div#debug{background-color:hsl(210,7%,5%);color:hsl(120,40%,45%);border-color:hsla(120,60%,70%,.2);margin-top:4rem;margin-bottom:4rem}body.novaconium .pages-table{width:100%;border:1px solid hsla(120,60%,70%,.2)}body.novaconium .pages-table th,body.novaconium .pages-table td{border:1px solid hsla(120,60%,70%,.2);text-align:left}body.novaconium .pages-table th{background-color:rgba(0,178.5,0,.05);color:hsl(120,52%,15%);font-weight:600}body.novaconium #edit-page-form-novaconium .form-group{margin-bottom:1rem}body.novaconium #edit-page-form-novaconium .form-group.fullwidth textarea{width:100%;resize:vertical}body.novaconium #edit-page-form-novaconium .checkbox-group{margin-top:1.5rem}body.novaconium #edit-page-form-novaconium input[type=text],body.novaconium #edit-page-form-novaconium input[type=number],body.novaconium #edit-page-form-novaconium textarea,body.novaconium #edit-page-form-novaconium select{background-color:hsl(210,7%,5%);border:1px solid hsla(120,60%,70%,.2);color:#fff;padding:.5rem;border-radius:4px;font-family:"VT323","Courier New","SF Mono","Fira Code",Consolas,monospace}body.novaconium #edit-page-form-novaconium input[type=text]:focus,body.novaconium #edit-page-form-novaconium input[type=number]:focus,body.novaconium #edit-page-form-novaconium textarea:focus,body.novaconium #edit-page-form-novaconium select:focus{border-color:rgb(0,178.5,0);box-shadow:0 0 0 .25rem rgba(0,178.5,0,.3);outline:none}body.novaconium #edit-page-form-novaconium input[type=checkbox]{accent-color:rgb(0,178.5,0)}body.novaconium #edit-page-form-novaconium button[type=submit]{background-color:rgb(0,178.5,0);color:hsl(0,0%,2%);border:none;padding:.5rem 1rem;border-radius:4px;cursor:pointer;font-family:"VT323","Courier New","SF Mono","Fira Code",Consolas,monospace}body.novaconium #edit-page-form-novaconium button[type=submit]:hover,body.novaconium #edit-page-form-novaconium button[type=submit]:focus{background-color:#090;box-shadow:0 0 4px rgba(0,178.5,0,.5)}body.novaconium #edit-page-form-novaconium p{color:hsl(120,40%,45%)}body.novaconium #edit-page-form-novaconium a{color:rgb(0,178.5,0)}body.novaconium #edit-page-form-novaconium a:hover{color:hsl(120,52%,15%);text-decoration:underline}body.novaconium #login{padding:0}body.novaconium #login input[type=text],body.novaconium #login input[type=password],body.novaconium #login button[type=submit]{width:100%;max-width:300px;padding:1rem;margin-bottom:1rem;box-sizing:border-box;font-size:1rem;border:1px solid hsla(120,60%,70%,.2);border-radius:4px;background:hsl(0,0%,2%);color:#fff}body.novaconium #login input[type=text]{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2'/%3E%3Ccircle cx='12' cy='7' r='4'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:1rem center;background-size:20px;padding-left:3rem}body.novaconium #login input[type=password]{background-image:url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='3' y='11' width='18' height='11' rx='2' ry='2'/%3E%3Cpath d='M7 11V7a5 5 0 0 1 10 0v4'/%3E%3C/svg%3E");background-repeat:no-repeat;background-position:1rem center;background-size:20px;padding-left:3rem}body.novaconium #login button[type=submit]{background:rgb(0,178.5,0);border-color:rgb(0,178.5,0);cursor:pointer;transition:background .2s}body.novaconium #login button[type=submit]:hover{background:rgb(0,178.5,0);text-shadow:0 0 5px rgba(0,178.5,0,.5)}body.novaconium #login button[type=submit]::after{content:" →";margin-left:.5rem}body.novaconium #biglogo{position:relative;display:inline-block;padding:20px 0;border-radius:0;overflow:hidden}body.novaconium #biglogo::before{content:"";position:absolute;top:0;left:0;right:0;bottom:0;background-image:linear-gradient(rgba(0, 255, 0, 0.02) 1px, transparent 1px),linear-gradient(90deg, rgba(0, 255, 0, 0.02) 1px, transparent 1px);background-size:20px 20px;opacity:.1;pointer-events:none}body.novaconium #biglogo::after{content:"";position:absolute;top:0;left:0;right:0;height:1px;background:linear-gradient(90deg, transparent, #00ff00, transparent);opacity:.05;animation:scan 3s linear infinite}body.novaconium #biglogo .main{display:block;font-family:"Orbitron",sans-serif;font-size:4.5rem;font-weight:900;color:#fff;letter-spacing:.1em;line-height:.85;text-transform:uppercase;text-shadow:4px 4px 0 #222,5px 5px 0 #111,-1px -1px 0 lime,1px 1px 20px rgba(0,255,0,.3);filter:drop-shadow(2px 2px 4px rgba(0, 0, 0, 0.8))}body.novaconium #biglogo .sub{display:block;font-family:"Orbitron",sans-serif;font-size:2.5rem;font-weight:700;color:#fff;letter-spacing:.15em;margin-top:.5rem;text-shadow:2px 2px 0 #222,-1px -1px 0 lime,1px 1px 10px rgba(0,255,0,.2);filter:drop-shadow(1px 1px 2px rgba(0, 0, 0, 0.6))}body.novaconium #biglogo:hover .main{animation:glitch .3s infinite}@keyframes scan{0%{top:-1px}100%{top:100%}}@keyframes glitch{0%,100%{transform:translate(0)}20%{transform:translate(-2px, 2px)}40%{transform:translate(-2px, -2px)}60%{transform:translate(2px, 2px)}80%{transform:translate(2px, -2px)}}body#controlPanel .tab-container{margin:1.5rem auto;background:hsl(0,0%,2%)}body#controlPanel .tab-nav{display:flex;background-color:hsl(210,7%,5%);border-bottom:1px solid hsla(120,60%,70%,.2)}body#controlPanel .tab-button{padding:.5rem 1rem;background:hsl(210,7%,5%);border:1px solid hsla(120,60%,70%,.2);border-bottom:none;cursor:pointer;font-size:14px;min-width:120px;text-align:center;color:#fff;font-family:"Courier New","SF Mono","Fira Code",Consolas,monospace}body#controlPanel .tab-button:hover{background-color:hsl(210,7%,10%)}body#controlPanel .tab-button+.tab-button{border-left:none}body#controlPanel .tab-button.active{background-color:hsl(0,0%,2%);border-bottom:2px solid rgb(0,178.5,0);color:rgb(0,178.5,0);font-weight:bold}body#controlPanel .tab-content{display:none;padding:1.5rem;background-color:hsl(0,0%,2%);color:#fff;border:none}body#controlPanel .tab-content.active{display:block}#edit-page-title{margin-top:2rem}#edit-page-title #title{width:100%;font-size:1.9rem;padding:20px}#edit-page-title>div{font-size:.8rem;margin:8px 0 0 20px}body.novaconium .tooltip{position:relative;display:inline-block;cursor:help;font-size:16px;color:rgb(0,178.5,0);margin-left:.25rem;top:2px}body.novaconium .tooltip .tooltiptext{visibility:hidden;width:200px;background-color:hsl(210,7%,5%);color:#fff;text-align:left;border-radius:0;padding:.5rem;position:fixed;z-index:1000;opacity:0;transition:opacity .3s;font-size:14px;box-shadow:0 2px 5px rgba(0,0,0,.2);pointer-events:none}body.novaconium .tooltip .tooltiptext::after{content:"";position:absolute;top:-5px;left:10px;border-width:5px;border-style:solid;border-color:hsl(210,7%,5%) rgba(0,0,0,0) rgba(0,0,0,0) rgba(0,0,0,0)}body.novaconium .tooltip:hover .tooltiptext{visibility:visible;opacity:1}body.novaconium .tooltip-desc{position:absolute;left:-9999px}.editor-container{width:100%;min-height:400px}.ace-editor{height:400px;border:1px solid rgb(0,178.5,0);font-family:"Courier New","SF Mono","Fira Code",Consolas,monospace}.ace-editor .ace_gutter{background:hsl(210,7%,5%) !important;color:hsl(120,40%,45%) !important;border-right:1px solid rgb(0,178.5,0) !important}.ace-editor .ace_scroller{background:hsl(0,0%,2%) !important}.ace-editor .ace_text-layer .ace_print-margin{background:rgba(0,0,0,0) !important}.ace-editor .ace_marker-layer .ace_selection{background:rgba(0,178.5,0,.7) !important}body.novaconium .tags-dropdown{position:absolute;top:100%;left:0;right:0;max-height:200px;overflow-y:auto;background:hsl(210,7%,5%);border:1px solid hsla(120,60%,70%,.2);border-top:none;border-radius:0 0 4px 4px;list-style:none;margin:0;padding:0;z-index:10;opacity:0;visibility:hidden;transition:opacity .2s}body.novaconium .tags-dropdown[aria-expanded=true]{opacity:1;visibility:visible}body.novaconium .tags-dropdown li{padding:.25rem .5rem;cursor:pointer;color:#fff;font-size:14px;border-bottom:1px solid rgba(0,0,0,0)}body.novaconium .tags-dropdown li:hover,body.novaconium .tags-dropdown li.selected{background:rgba(0,178.5,0,.9);color:rgb(0,178.5,0)}body.novaconium .tags-dropdown li:last-child{border-bottom:none}body.novaconium .tags-input{position:relative}body.novaconium .tags-input{display:flex;flex-wrap:wrap;gap:.25rem;align-items:center;border:1px solid hsla(120,60%,70%,.2);padding:.25rem;background:hsl(210,7%,5%);min-height:40px;grid-column:2}body.novaconium .tags-input input{border:none;background:rgba(0,0,0,0);color:#fff;font-family:"Courier New","SF Mono","Fira Code",Consolas,monospace;font-size:14px;flex:1;min-width:100px;outline:none}body.novaconium .tags-input input::placeholder{color:hsl(120,40%,45%)}body.novaconium .tag-chip{display:inline-flex;align-items:center;background:rgba(0,178.5,0,.8);color:#fff;padding:.25rem .5rem;border-radius:4px;font-size:12px;max-width:150px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}body.novaconium .tag-chip .tag-remove{background:none;border:none;color:inherit;font-size:16px;margin-left:.25rem;cursor:pointer;padding:0;line-height:1}body.novaconium .tag-chip .tag-remove:hover{opacity:.7}body.novaconium .tags-input input[list]::-webkit-calendar-picker-indicator{display:none}body#controlPanel header{padding:0}body#controlPanel h1#biglogo{padding:10px 0 0 0;margin:0}body#controlPanel div#panel{display:flex;flex-direction:row}body#controlPanel div#panel div#cp-menu{background-color:#000;width:360px;padding:0;white-space:nowrap}body#controlPanel div#panel div#cp-menu ul#cp-nav{list-style:none;margin:0;padding:20px 0 0 0}body#controlPanel div#panel div#cp-menu ul#cp-nav li a{display:block;border-bottom:1px solid hsla(120,60%,70%,.2);padding:4px 10px}body#controlPanel div#panel div#cp-menu ul#cp-nav li a:hover{background-color:#222}body#controlPanel div#panel main#content{flex:1;padding:20px 40px;min-width:0;background-color:rgba(11,11,11,.8392156863);border-left:2px solid #104910}body#controlPanel footer{padding:1rem}body#controlPanel h1{font-family:"VT323","Courier New","SF Mono","Fira Code",Consolas,monospace;font-size:48px}body#coming-soon{display:flex;justify-content:center;align-items:center;height:100%;padding:1rem;box-sizing:border-box;margin:0;text-align:center;color:#eee;font-family:"Segoe UI",Roboto,sans-serif;background-size:cover;background-repeat:no-repeat;background-position:center center;background-attachment:fixed}body#coming-soon .container{max-width:600px;background-color:rgba(30,30,30,.89);padding:2rem;border-radius:8px;box-shadow:0 0 20px rgba(0,0,0,.5)}body#coming-soon .container h1{font-size:3rem;margin-bottom:1rem;animation:pulse 1.5s infinite}body#coming-soon .container p{font-size:1.2rem;opacity:.85;margin-bottom:1.5rem}body#coming-soon .container .countdown{font-size:2rem;font-weight:bold;margin:1rem 0 2rem 0}body#coming-soon .container form.newsletter{display:flex;flex-direction:column;gap:.5rem;margin-top:1rem}body#coming-soon .container form.newsletter input[type=email]{padding:.5rem;font-size:1rem;border:1px solid #666;border-radius:4px;background:hsla(0,0%,100%,.05);color:#eee}body#coming-soon .container form.newsletter button{padding:.5rem;font-size:1rem;border:1px solid #1e90ff;border-radius:4px;background:#1e90ff;color:#fff;cursor:pointer;transition:background .2s}body#coming-soon .container form.newsletter button:hover{background:#1565c0}body#coming-soon .container .social-icons{margin-top:2rem;display:flex;justify-content:center;gap:1rem}body#coming-soon .container .social-icons a{display:inline-flex;align-items:center;justify-content:center;width:40px;height:40px;font-size:1.5rem;color:#eee;text-decoration:none;transition:color .2s}body#coming-soon .container .social-icons a:hover{color:#1e90ff}body#coming-soon .container .social-icons i{font-style:normal}@keyframes pulse{0%,100%{opacity:.8}50%{opacity:1}}/*# sourceMappingURL=novaconium.css.map */ diff --git a/skeleton/novaconium/public/index.php b/skeleton/novaconium/public/index.php deleted file mode 100644 index eb86c3b..0000000 --- a/skeleton/novaconium/public/index.php +++ /dev/null @@ -1,19 +0,0 @@ - content.classList.remove('active')); - - const buttons = document.querySelectorAll('.tab-button'); - buttons.forEach(b => b.classList.remove('active')); - - document.getElementById(tabId).classList.add('active'); - button.classList.add('active'); - - if (tabId === 'content6') { - setTimeout(initTags, 0); - } -} - -// Tags Init (with custom dropdown autocomplete) -let tagsListeners = []; - -function initTags() { - const tagsInput = document.getElementById('tags-input'); - const tagsField = document.getElementById('tags'); - const hiddenTags = document.getElementById('tags_json'); - const dropdown = document.getElementById('tags-dropdown'); - - if (!tagsInput || !tagsField || !hiddenTags || !dropdown) { - console.warn('Tags elements missing'); - return; - } - - // Remove old listeners - tagsListeners.forEach(ls => ls()); - tagsListeners = []; - - let tags = []; - let existingTags = []; - try { - tags = JSON.parse(tagsInput.dataset.tags || '[]'); - existingTags = JSON.parse(tagsInput.dataset.existingTags || '[]'); - } catch (e) { - console.warn('JSON error:', e); - } - - let selectedIndex = -1; - - function renderTags() { - tagsInput.innerHTML = ''; - tags.forEach((tag, index) => { - const chip = document.createElement('span'); - chip.className = 'tag-chip'; - chip.innerHTML = `${tag} `; - chip.querySelector('.tag-remove').onclick = () => { - tags.splice(index, 1); - renderTags(); - hiddenTags.value = JSON.stringify(tags); - }; - tagsInput.appendChild(chip); - }); - tagsInput.appendChild(tagsField); - tagsInput.appendChild(dropdown); - hiddenTags.value = JSON.stringify(tags); - } - - function updateDropdown() { - const value = tagsField.value.toLowerCase().trim(); - dropdown.innerHTML = ''; - dropdown.setAttribute('aria-expanded', value ? 'true' : 'false'); - selectedIndex = -1; - - if (!value) return; - - const matches = existingTags - .filter(tag => tag.toLowerCase().startsWith(value) && !tags.includes(tag.toLowerCase())) - .slice(0, 10); - - matches.forEach((tag, index) => { - const li = document.createElement('li'); - li.textContent = tag; - li.setAttribute('role', 'option'); - li.onclick = () => selectTag(tag); - li.onmouseover = () => { selectedIndex = index; updateHighlight(); }; - dropdown.appendChild(li); - }); - - if (matches.length) dropdown.parentElement.classList.add('has-dropdown'); - else dropdown.parentElement.classList.remove('has-dropdown'); - } - - function updateHighlight() { - dropdown.querySelectorAll('li').forEach((li, index) => { - li.classList.toggle('selected', index === selectedIndex); - }); - } - - function selectTag(tag) { - addTag(tag, true); - tagsField.value = ''; - dropdown.setAttribute('aria-expanded', 'false'); - } - - function addTag(inputValue, refocus = false) { - const tag = inputValue.trim().toLowerCase().replace(/[^\w-]/g, ''); - if (tag && tag.length > 0 && tag.length <= 50 && !tags.includes(tag)) { - tags.push(tag); - renderTags(); - if (refocus) tagsField.focus(); - } - tagsField.value = ''; - updateDropdown(); - } - - const keydownListener = (e) => { - if (dropdown.getAttribute('aria-expanded') === 'true') { - if (e.key === 'ArrowDown') { - e.preventDefault(); - selectedIndex = Math.min(selectedIndex + 1, dropdown.querySelectorAll('li').length - 1); - updateHighlight(); - dropdown.querySelector(`li:nth-child(${selectedIndex + 1})`)?.scrollIntoView({ block: 'nearest' }); - } else if (e.key === 'ArrowUp') { - e.preventDefault(); - selectedIndex = Math.max(selectedIndex - 1, 0); - updateHighlight(); - dropdown.querySelector(`li:nth-child(${selectedIndex + 1})`)?.scrollIntoView({ block: 'nearest' }); - } else if ((e.key === 'Enter' || e.key === 'Tab') && selectedIndex >= 0) { - e.preventDefault(); - const selected = dropdown.querySelector(`li:nth-child(${selectedIndex + 1})`)?.textContent; - if (selected) selectTag(selected); - } else if (e.key === 'Escape') { - e.preventDefault(); - tagsField.blur(); - } - } - - if (!dropdown.getAttribute('aria-expanded') === 'true' || selectedIndex < 0) { - if (e.key === 'Enter' || e.key === ',') { - e.preventDefault(); - addTag(tagsField.value, true); - } else if (e.key === 'Tab') { - e.preventDefault(); - if (tagsField.value.trim()) addTag(tagsField.value, true); - } - } - }; - - tagsField.addEventListener('keydown', keydownListener); - tagsListeners.push(() => tagsField.removeEventListener('keydown', keydownListener)); - - const inputListener = () => updateDropdown(); - tagsField.addEventListener('input', inputListener); - tagsListeners.push(() => tagsField.removeEventListener('input', inputListener)); - - const blurListener = () => { - setTimeout(() => dropdown.setAttribute('aria-expanded', 'false'), 150); - if (tagsField.value.trim()) addTag(tagsField.value); - }; - tagsField.addEventListener('blur', blurListener); - tagsListeners.push(() => tagsField.removeEventListener('blur', blurListener)); - - renderTags(); -} - -// Init on load and observe tab changes -document.addEventListener('DOMContentLoaded', () => { - const tagsTab = document.getElementById('content6'); - if (tagsTab?.classList.contains('active')) { - initTags(); - } - - const observer = new MutationObserver((mutations) => { - mutations.forEach((mutation) => { - if (mutation.type === 'attributes' && mutation.attributeName === 'class') { - const target = mutation.target; - if (target.id === 'content6' && target.classList.contains('active')) { - initTags(); - } - } - }); - }); - observer.observe(document.body, { subtree: true, attributes: true }); -}); - -// Export switchTab for external use if needed -window.switchTab = switchTab; diff --git a/skeleton/novaconium/sass/project.sass b/skeleton/novaconium/sass/project.sass deleted file mode 100644 index 1bacbc3..0000000 --- a/skeleton/novaconium/sass/project.sass +++ /dev/null @@ -1 +0,0 @@ -@use '../vendor/4lt/novaconium/sass/novaconium' diff --git a/src/Database.php b/src/Database.php deleted file mode 100644 index 7689080..0000000 --- a/src/Database.php +++ /dev/null @@ -1,127 +0,0 @@ -conn = new \mysqli($dbinfo['host'], $dbinfo['user'], $dbinfo['pass'], $dbinfo['name']); - if ($this->conn->connect_error) { - die("Connection failed: " . $this->conn->connect_error); - } - - } - - public function query($query, $params = []) { - // Clean up pending results to avoid "commands out of sync" - while ($this->conn->more_results() && $this->conn->next_result()) { - if ($res = $this->conn->use_result()) { - $res->free(); - } - } - - // Prepare the SQL statement - $stmt = $this->conn->prepare($query); - if (!$stmt) { - throw new Exception("Query preparation failed: " . $this->conn->error); - } - - // Bind parameters if needed - if (!empty($params)) { - $types = str_repeat('s', count($params)); // Use 's' for all types, or detect types dynamically - $stmt->bind_param($types, ...$params); - } - - // Execute the statement - if (!$stmt->execute()) { - $stmt->close(); - throw new Exception("Query execution failed: " . $stmt->error); - } - - // Save last insert id if it's an INSERT query - if (preg_match('/^\s*INSERT/i', $query)) { - $this->lastid = $this->conn->insert_id; - } else { - $this->lastid = 0; - } - - // Decide what to return - // For SELECT/SHOW etc., return result set - if (preg_match('/^\s*(SELECT|SHOW|DESCRIBE|EXPLAIN)/i', $query)) { - $result = $stmt->get_result(); - $stmt->close(); - return $result; - } - - // For INSERT/UPDATE/DELETE, return success status - $success = $stmt->affected_rows; - $stmt->close(); - return $success; - } - - public function lastid() { - return $this->lastid; - } - - public function getRow($query, $params = []) { - try { - // Prepare the SQL statement - $stmt = $this->conn->prepare($query); - if (!$stmt) { - throw new Exception("Query preparation failed: " . $this->conn->error); - } - - // Bind parameters - if (!empty($params)) { - $types = str_repeat('s', count($params)); // You may improve this with actual type detection - $stmt->bind_param($types, ...$params); - } - - // Execute the statement - if (!$stmt->execute()) { - $stmt->close(); - throw new Exception("Query execution failed: " . $stmt->error); - } - - // Get result - $result = $stmt->get_result(); - $row = $result->fetch_assoc(); - - $stmt->close(); - return $row; - - } catch (Exception $e) { - echo "An error occurred: " . $e->getMessage(); - return null; - } - } - - - public function getRows($query, $params = []) { - $stmt = $this->conn->prepare($query); - if (!$stmt) { - die("Query preparation failed: " . $this->conn->error); - } - - // Bind parameters if provided - if (!empty($params)) { - $types = str_repeat('s', count($params)); // Assuming all are strings, adjust as needed - $stmt->bind_param($types, ...$params); - } - - $stmt->execute(); - $result = $stmt->get_result(); // Requires MySQL Native Driver (mysqlnd) - - if ($result) { - return $result->fetch_all(MYSQLI_ASSOC); - } else { - return []; - } - } - - public function close() { - $this->conn->close(); - } -} \ No newline at end of file diff --git a/src/Logger.php b/src/Logger.php deleted file mode 100644 index 01d4e4b..0000000 --- a/src/Logger.php +++ /dev/null @@ -1,54 +0,0 @@ - 0, - 'INFO' => 1, - 'WARNING' => 2, - 'ERROR' => 3, - 'NONE' => 999 - ]; - - public function __construct(string $logFile, string $minLevel = 'DEBUG') { - $this->logFile = $logFile; - $minLevel = strtoupper($minLevel); - $this->logLevelThreshold = self::LEVELS[$minLevel] ?? 0; - } - - protected function log(string $level, string $message): void { - $level = strtoupper($level); - if (!isset(self::LEVELS[$level]) || self::LEVELS[$level] < $this->logLevelThreshold) { - return; - } - - $time = date('Y-m-d H:i:s'); - $ip = $_SERVER['REMOTE_ADDR']; - $logEntry = "[$time] [$ip] [$level] $message" . PHP_EOL; - - file_put_contents($this->logFile, $logEntry, FILE_APPEND | LOCK_EX); - } - - public function debug(string $msg): void { - $this->log('DEBUG', $msg); - } - - public function info(string $msg): void { - $this->log('INFO', $msg); - } - - public function warning(string $msg): void { - $this->log('WARNING', $msg); - } - - // Alias - public function warn(string $msg): void { - $this->log('WARNING', $msg); - } - - public function error(string $msg): void { - $this->log('ERROR', $msg); - } -} diff --git a/src/MessageHandler.php b/src/MessageHandler.php deleted file mode 100644 index b28cd68..0000000 --- a/src/MessageHandler.php +++ /dev/null @@ -1,87 +0,0 @@ - [], - 'warning' => [], - 'notice' => [], - 'success' => [] - ]; - - public function __construct(array $sessionMessages = []) - { - // Merge existing session messages into the default structure - foreach ($this->messages as $type => $_) { - if (isset($sessionMessages[$type]) && is_array($sessionMessages[$type])) { - $this->messages[$type] = $sessionMessages[$type]; - } - } - } - - // Add a message of a specific type - public function addMessage($type, $message) { - if (!isset($this->messages[$type])) { - throw new Exception("Invalid message type: $type"); - } - $this->messages[$type][] = $message; - } - - public function error($message){ - $this->addMessage('error', $message); - } - - public function notice($message){ - $this->addMessage('notice', $message); - } - - // Get all messages of a specific type - public function getMessages($type) { - return $this->messages[$type] ?? []; - } - - // Get all messages of a specific type - public function showMessages($type) { - $result = $this->messages[$type] ?? []; - $this->messages[$type] = []; // Clear messages after showing - return $result; - } - - // Get all messages of all types - public function getAllMessages() { - return $this->messages; - } - - // Get the count of messages for a specific type - public function count($type) { - return isset($this->messages[$type]) ? count($this->messages[$type]) : 0; - } - - // Get the total count of all messages - public function totalCount() { - return array_sum(array_map('count', $this->messages)); - } - - // Check if there are any messages of a specific type - public function hasMessages($type) { - return !empty($this->messages[$type]); - } - - // Check if there are any messages at all - public function hasAnyMessages() { - return $this->totalCount() > 0; - } - - // Clear messages of a specific type - public function clear($type) { - if (isset($this->messages[$type])) { - $this->messages[$type] = []; - } - } - - // Clear all messages - public function clearAll() { - foreach ($this->messages as $type => $list) { - $this->messages[$type] = []; - } - } -} diff --git a/src/Post.php b/src/Post.php deleted file mode 100644 index daf3296..0000000 --- a/src/Post.php +++ /dev/null @@ -1,25 +0,0 @@ -sanitize($post); - } - - private function sanitize($post) { - foreach ($post as $key => $value) { - $this->data[$key] = is_array($value) - ? filter_var_array($value, FILTER_SANITIZE_FULL_SPECIAL_CHARS) - : filter_var($value, FILTER_SANITIZE_FULL_SPECIAL_CHARS); - } - } - - public function get($key, $default = null) { - return $this->data[$key] ?? $default; - } - - public function all() { - return $this->data; - } -} diff --git a/src/Redirect.php b/src/Redirect.php deleted file mode 100644 index 9a48c11..0000000 --- a/src/Redirect.php +++ /dev/null @@ -1,40 +0,0 @@ -url('/login'); - * to trigger a redirect - */ - - -class Redirect { - private ?string $url = null; - private int $statusCode = 303; - - public function url(string $relativeUrl, int $statusCode = 303): void { - $this->statusCode = $statusCode; - - // Detect HTTPS - $protocol = (!empty($_SERVER['HTTPS']) && $_SERVER['HTTPS'] !== 'off') ? "https" : "http"; - - // Get Hostname - $host = $_SERVER['HTTP_HOST']; - - // Get Base Directory - $basePath = rtrim(dirname($_SERVER['SCRIPT_NAME']), '/\\'); - - // Construct Absolute URL - $this->url = "$protocol://$host$basePath/" . ltrim($relativeUrl, '/'); - } - - public function isset(): bool { - return !is_null($this->url); - } - - public function execute(): void { - if ($this->url) { - header("Location: " . $this->url, true, $this->statusCode); - exit(); - } - } -} diff --git a/src/Router.php b/src/Router.php deleted file mode 100644 index 1cf5f06..0000000 --- a/src/Router.php +++ /dev/null @@ -1,142 +0,0 @@ -routes = $this->loadRoutes(); - $this->path = $this->preparePath(); - $this->query = $this->prepareQuery(); - $this->requestType = $this->getRequestType(); - $this->controller = $this->findController(); - $this->controllerPath = $this->setRouteFile(); - } - - private function loadRoutes() { - require_once( \FRAMEWORKPATH . '/config/routes.php'); - // Check if Path exists - if (file_exists(\BASEPATH . '/App/routes.php')) { - require_once( \BASEPATH . '/App/routes.php'); - } - $routes = array_merge((array)$routes, (array)$framework_routes); - - return $routes; - } - - private function preparePath() { - $path = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH); - - //homepage - if ($path === '/') { - return $path; - } - - // remove empty directory path - $path = rtrim($path, '/'); // remove trailing slash - - //remove anything after and including ampersand - $path = preg_replace('/&.+$/', '', $path); - - return $path; - } - - private function prepareQuery() { - $parsedUri = parse_url($_SERVER['REQUEST_URI']); - $queryArray = []; - if (isset($parsedUri['query'])) { - parse_str($parsedUri['query'], $queryArray); - } - return $queryArray; - } - - private function getRequestType() { - // is the request a get or post? - if (empty($_POST)) { - return 'get'; - } else { - return 'post'; - } - } - - private function findController() { - - // one to one match - if (array_key_exists($this->path, $this->routes)) { - if (!empty($this->routes[$this->path][$this->requestType])) { - return $this->routes[$this->path][$this->requestType]; - } - } - - foreach ($this->routes as $key => $value) { - // Check if key contains a curly bracket, if not continue. We already checked above. - if (strpos($key, '{') === false) continue; - - // Remove everything after the curly bracket, from key - $keyPath = substr($key, 0, strpos($key, '{')); - - //see if keyPath matches the first characters of $this->path, only the first characters have to match - if (strpos($this->path, $keyPath) === 0) { - - // We have a potential match. Now check if the parameter count is equal - $keyParams = explode('/', $key); - $pathParams = explode('/', $this->path); - $keyParamCount = count($keyParams); - $pathParamCount = count($pathParams); - if ($keyParamCount === $pathParamCount) { - for ($i=0; $i < $pathParamCount; $i++) { - if (strpos($keyParams[$i], '{') !== false) { - $keyParams[$i] = substr($keyParams[$i], 1, -1); - $this->parameters[$keyParams[$i]] = $pathParams[$i]; - return $this->routes[$key][$this->requestType]; - } - } - } - } - } - - return '404'; - - } - - // checks if the file exists, sets file path - private function setRouteFile() { - - if (str_starts_with($this->controller, 'NOVACONIUM')) { - $trimmed = substr($this->controller, strlen('NOVACONIUM/')); - $cp = \FRAMEWORKPATH . '/controllers/' . $trimmed . '.php'; - } else { - $cp = \BASEPATH . '/App/controllers/' . $this->controller . '.php'; - } - - if (file_exists($cp)) { - return $cp; - } else { - //Check if 404 exits - if (file_exists( \BASEPATH . '/App/controllers/404.php')) { - return \BASEPATH . '/App/controllers/404.php'; - } else { - return \FRAMEWORKPATH . '/controllers/404.php'; - } - } - } - - public function debug() { - echo '
      '; - echo ''; - echo ''; - echo ''; - echo ''; - echo ''; - echo '
      Url Path' . htmlspecialchars($this->path) . '
      Controller Path' . htmlspecialchars($this->controllerPath) . '
      Parameters
      ' . print_r($this->parameters, true) . '
      Routes
      ' . print_r($this->routes, true) . '
      '; - - die(); - } - - -} \ No newline at end of file diff --git a/src/Services/Auth.php b/src/Services/Auth.php deleted file mode 100644 index 36ab24c..0000000 --- a/src/Services/Auth.php +++ /dev/null @@ -1,10 +0,0 @@ -db = $db; - } - - /** - * Assign tags to a page. - * - * This will delete old links and insert new ones. - * - * @param int $pageId - * @param array $tags Array of tag names - */ - public function setTagsForPage(int $pageId, array $tags): void - { - // Remove existing links - $this->db->query("DELETE FROM page_tags WHERE page_id = ?", [$pageId]); - - foreach ($tags as $tagName) { - $tagName = trim($tagName); - if ($tagName === '') continue; - - // Check if tag exists - $stmt = $this->db->query("SELECT id FROM tags WHERE name = ?", [$tagName]); - $row = $stmt->fetch_assoc(); // mysqli_result -> assoc array - if ($row) { - $tagId = $row['id']; - } else { - // Insert new tag - $this->db->query( - "INSERT INTO tags (name, created) VALUES (?, NOW())", - [$tagName] - ); - $tagId = $this->db->lastid; - } - - // Link page to tag - $this->db->query( - "INSERT INTO page_tags (page_id, tag_id) VALUES (?, ?)", - [$pageId, $tagId] - ); - } - } -} diff --git a/src/Session.php b/src/Session.php deleted file mode 100644 index d3f574f..0000000 --- a/src/Session.php +++ /dev/null @@ -1,63 +0,0 @@ -session = &$_SESSION; // Reference $_SESSION to keep them in sync - if (!isset($this->session['token'])) { - $this->setToken(); - } - if (!isset($this->session['messages'])) { - $this->session['messages'] = []; // Always ensure messages is an array - } - if (!isset($this->session['formData'])) { - $this->session['formData'] = []; // Initialize formData - } - if (!isset($this->session['errors'])) { - $this->session['errors'] = []; // Initialize errors - } - } - - public function setToken() { - $this->session['token'] = bin2hex(random_bytes(32)); - } - - public function set($key, $value) { - $this->session[$key] = $value; - } - - public function get($key) { - return isset($this->session[$key]) ? $this->session[$key] : null; - } - - public function flash($key) { - $return = $this->get($key); - $this->delete($key); - return $return; - } - - public function debug() { - return $this->session; - } - - public function delete($key) { - if (isset($this->session[$key])) { - unset($this->session[$key]); - } - } - - public function write() { - // No need to assign to $_SESSION since $this->session is a reference - session_write_close(); - } - - public function kill() { - $this->session = []; - $_SESSION = []; - session_destroy(); - } -} \ No newline at end of file diff --git a/src/functions.php b/src/functions.php deleted file mode 100644 index a7743ab..0000000 --- a/src/functions.php +++ /dev/null @@ -1,60 +0,0 @@ -"; - foreach ($vars as $var) { - var_dump($var); - echo "\n"; - } - echo ""; - exit; -} - -/** - * Finalize the request lifecycle. - * - * This function safely writes session data, stores flash messages, closes the database - * connection if configured, and performs a redirect. - */ -function makeitso(): void { - global $session, $db, $redirect, $config, $messages, $log; - - // ------------------------------ - // Close database if configured - // ------------------------------ - - // Check if database configuration exists and the $db object is set - if (!empty($config['database']['host']) && isset($db)) { - // If the $db object has a close method, call it to close the connection - if (method_exists($db, 'close')) { - $db->close(); - } - } - - // ------------------------------ - // Save flash messages to session - // ------------------------------ - - // Set all messages in the session - $session->set('messages', $messages->getAllMessages()); - - // Write any buffered session data to persistent storage - $session->write(); - - // ------------------------------ - // Perform redirect - // ------------------------------ - - // Execute the redirect operation - $redirect->execute(); - - // Exit the script after processing is complete - exit; -} \ No newline at end of file diff --git a/src/novaconium.php b/src/novaconium.php deleted file mode 100644 index 5f2c5a9..0000000 --- a/src/novaconium.php +++ /dev/null @@ -1,59 +0,0 @@ -get('token'); -$data['username'] = $session->get('username'); - -// --- Messages --- -$messages = new MessageHandler($session->flash('messages')); -foreach (['error', 'notice'] as $key) { - $data[$key] = $messages->showMessages($key); -} - -// --- Database --- -if (!empty($config['database']['host'])) { - $db = new Database($config['database']); -} - -// --- POST Wrapper --- -if (!empty($_POST)) { - $post = new Post($_POST); -} - -// --- Redirect Handler --- -$redirect = new Redirect(); - -// --- Router --- -$router = new Router(); -require_once $router->controllerPath; diff --git a/src/twig.php b/src/twig.php deleted file mode 100644 index 1611aea..0000000 --- a/src/twig.php +++ /dev/null @@ -1,69 +0,0 @@ -addPath(FRAMEWORKPATH . '/twig', 'novaconium'); - } - - if (is_dir(FRAMEWORKPATH . '/views')) { - $loader->addPath(FRAMEWORKPATH . '/views', 'novacore'); - } - - if (is_dir(BASEPATH . '/App/templates')) { - $loader->addPath(BASEPATH . '/App/templates', 'override'); - } - - $twig = new Environment($loader); - - $twig->addGlobal('config', $config); - - // ---------------------------------------- - // Render template - // ---------------------------------------- - - $appTemplatePath = BASEPATH . '/App/views/' . $name . '.html.twig'; - - if (file_exists($appTemplatePath)) { - echo $twig->render($name . '.html.twig', $data); - return true; - } - - if (str_starts_with($name, '@')) { - echo $twig->render($name . '.html.twig', $data); - return true; - } - - echo "Error: Twig Template ($name) Not Found."; - return false; -} diff --git a/twig/coming-soon/index.html.twig b/twig/coming-soon/index.html.twig deleted file mode 100644 index fd15223..0000000 --- a/twig/coming-soon/index.html.twig +++ /dev/null @@ -1,71 +0,0 @@ - - - - - {{ title | default('Coming Soon') }} - - - - - - - -
      -

      {{ heading | default('Coming Soon') }}

      - {% block content %}{% endblock %} - - {% if countdown %} -
      - {% endif %} - - {# TODO: listmonk #} - - - {# Social media icons using Font Awesome #} - - -
      - - {% if countdown %} - - {% endif %} - - - diff --git a/twig/cp/control-panel.html.twig b/twig/cp/control-panel.html.twig deleted file mode 100644 index e76582a..0000000 --- a/twig/cp/control-panel.html.twig +++ /dev/null @@ -1,44 +0,0 @@ - - - - - {% include ['@novaconium/cp/head.html.twig'] %} - - - - -{# Page Header #} -
      -

      Novaconium

      -
      - - -
      - - {% include ['@novaconium/cp/menu.html.twig'] %} - -
      - {% block content %}{% endblock %} -
      - -
      - - {# Page Footer #} -
      - -
      - - {% if editor == 'ace' %} - {% include '@novaconium/javascript/page-edit.html.twig' %} - {% include '@novaconium/javascript/ace.html.twig' %} - {% endif %} - - - {% if debug is not empty %} -
      -

      Debugging Information

      - {{ debug|raw }} -
      - {% endif %} - - diff --git a/twig/cp/head.html.twig b/twig/cp/head.html.twig deleted file mode 100644 index ce68163..0000000 --- a/twig/cp/head.html.twig +++ /dev/null @@ -1,60 +0,0 @@ -{# ============================================================================= - - ============================================================================= -#} - - - -{{ title | default('Welcome To Novaconium') }} - - - -{# SEO & METADATA #} - - - - - -{# DARK MODE & THEME HINTS #} - - - -{# OPEN GRAPH (OG) FOR SOCIAL SHARING #} - - - - - -{# PWA & FAVICONS #} - - - - -{# GOOGLE FONTS (CDN VIA PRECONNECT) #} - - - - -{% if editor == 'ace' %} - - - - {# HTML syntax #} - {# Dark theme #} - {# Autocomplete #} - -{% endif %} - -{# highlight.js #} - - - - - - - -{# STYLESHEET #} - \ No newline at end of file diff --git a/twig/cp/menu.html.twig b/twig/cp/menu.html.twig deleted file mode 100644 index 69fde78..0000000 --- a/twig/cp/menu.html.twig +++ /dev/null @@ -1,9 +0,0 @@ - diff --git a/twig/foot.html.twig b/twig/foot.html.twig deleted file mode 100644 index e0fffd7..0000000 --- a/twig/foot.html.twig +++ /dev/null @@ -1,5 +0,0 @@ - diff --git a/twig/footer.html.twig b/twig/footer.html.twig deleted file mode 100644 index e8a3bc6..0000000 --- a/twig/footer.html.twig +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/twig/head.html.twig b/twig/head.html.twig deleted file mode 100644 index ae4cf78..0000000 --- a/twig/head.html.twig +++ /dev/null @@ -1,48 +0,0 @@ -{# ============================================================================= - - ============================================================================= -#} - - - -{{ title | default('Welcome To Novaconium') }} - - - -{# SEO & METADATA #} - - - - - -{# DARK MODE & THEME HINTS #} - - - -{# OPEN GRAPH (OG) FOR SOCIAL SHARING #} - - - - - -{# PWA & FAVICONS #} - - - - -{# GOOGLE FONTS (CDN VIA PRECONNECT) #} - - - - -{# STYLESHEET #} - - -{# highlight.js #} - - - - \ No newline at end of file diff --git a/twig/javascript/ace.html.twig b/twig/javascript/ace.html.twig deleted file mode 100644 index ae8003f..0000000 --- a/twig/javascript/ace.html.twig +++ /dev/null @@ -1,40 +0,0 @@ - - \ No newline at end of file diff --git a/twig/javascript/page-edit.html.twig b/twig/javascript/page-edit.html.twig deleted file mode 100644 index a44b722..0000000 --- a/twig/javascript/page-edit.html.twig +++ /dev/null @@ -1,192 +0,0 @@ - \ No newline at end of file diff --git a/twig/left.html.twig b/twig/left.html.twig deleted file mode 100644 index 05c2a85..0000000 --- a/twig/left.html.twig +++ /dev/null @@ -1,16 +0,0 @@ -
      - {% if pageclass == "novaconium" %} - - {% endif %} -
      diff --git a/twig/master.html.twig b/twig/master.html.twig deleted file mode 100644 index 53feba1..0000000 --- a/twig/master.html.twig +++ /dev/null @@ -1,91 +0,0 @@ - - - -{% include '@override/above_head.html.twig' ignore missing %} - - - {% include ['@override/head.html.twig', '@novaconium/head.html.twig'] %} - - - - - {# Page Header #} -
      - {% block headerbefore %}{% endblock %} - {% include ['@override/nav.html.twig', '@novaconium/nav.html.twig'] %} - {% block headerafter %}{% endblock %} -
      - - -
      -
      - - {% include ['@override/left.html.twig','@novaconium/left.html.twig'] %} - -
      - {% if error|default is not empty %} - {% for key, val in error %} -
      {{ val }}
      - {% endfor %} - {% endif %} - - {% if notice|default is not empty %} - {% for key, val in notice %} -
      {{ val }}
      - {% endfor %} - {% endif %} - -
      - {% block content %}{% endblock %} -
      - - {% include ['@override/right.html.twig','@novaconium/right.html.twig'] %} -
      - -
      -
      - - {# Page Footer #} -
      - {% block footerbefore %}{% endblock %} - {% include ['@override/footer.html.twig', '@novaconium/footer.html.twig'] %} - {% block footerafter %}{% endblock %} -
      - - {% if debug is not empty %} -
      -

      Debugging Information

      - {{ debug|raw }} -
      - {% endif %} - -{% include ['@override/foot.html.twig', '@novaconium/foot.html.twig'] %} -{% if matomo_id > 0 %} - - - -{% endif %} - - diff --git a/twig/nav.html.twig b/twig/nav.html.twig deleted file mode 100644 index 989ac06..0000000 --- a/twig/nav.html.twig +++ /dev/null @@ -1,5 +0,0 @@ -
      - -
      diff --git a/twig/right.html.twig b/twig/right.html.twig deleted file mode 100644 index c6d171a..0000000 --- a/twig/right.html.twig +++ /dev/null @@ -1 +0,0 @@ - diff --git a/views/404.html.twig b/views/404.html.twig deleted file mode 100644 index f875994..0000000 --- a/views/404.html.twig +++ /dev/null @@ -1,8 +0,0 @@ -{% set is_404 = status_code is defined and status_code == 404 %} -{% extends '@novaconium/master.html.twig' %} - -{% block content %} -

      404 File Not Found

      -

      Novaconium Default 404 page.

      -

      Return Home

      -{% endblock %} \ No newline at end of file diff --git a/views/auth/login.html.twig b/views/auth/login.html.twig deleted file mode 100644 index 0060822..0000000 --- a/views/auth/login.html.twig +++ /dev/null @@ -1,23 +0,0 @@ -{% extends '@novaconium/master.html.twig' %} - -{% block content %} -

      {{title}}

      - - -
      - -
      - - -
      -

      - -
      -

      - - -
      -
      - - -{% endblock %} diff --git a/views/coming-soon.html.twig b/views/coming-soon.html.twig deleted file mode 100644 index f0e1a7c..0000000 --- a/views/coming-soon.html.twig +++ /dev/null @@ -1,32 +0,0 @@ -{% extends '@novaconium/coming-soon/index.html.twig' %} - -{% set bg_image = "https://i.4lt.ca/4lt/waterBubbles.webp" %} -{% set default_launch = "now"|date_modify("+30 days")|date("Y-m-d\TH:i:s") %} - -{% block content %} -

      Our website is under construction. We’re working hard to bring you a better experience.

      -

      Stay tuned for updates!

      -{% endblock %} - -{% block social %} - - - - - - - - - - - - - - - - - - - - -{% endblock %} \ No newline at end of file diff --git a/views/dashboard.html.twig b/views/dashboard.html.twig deleted file mode 100644 index 1ecfba0..0000000 --- a/views/dashboard.html.twig +++ /dev/null @@ -1,8 +0,0 @@ -{% extends '@novaconium/cp/control-panel.html.twig' %} - -{% block content %} -

      {{title}}

      -

      Dashboard page

      -

      Homepage

      -

      logout

      -{% endblock %} diff --git a/views/editmessage.html.twig b/views/editmessage.html.twig deleted file mode 100644 index 208978f..0000000 --- a/views/editmessage.html.twig +++ /dev/null @@ -1,30 +0,0 @@ -{% extends '@novaconium/master.html.twig' %} - -{% block content %} -

      Edit Message - {{ title }}

      - -

      Delete

      - -
      - - - - - - - - - - - - - - -

      Created: {{ themessage.created|date("Y-m-d H:i:s") }}

      - - -
      -{% endblock %} diff --git a/views/editpage/index.html.twig b/views/editpage/index.html.twig deleted file mode 100644 index 9cecf02..0000000 --- a/views/editpage/index.html.twig +++ /dev/null @@ -1,62 +0,0 @@ -{% extends '@novaconium/cp/control-panel.html.twig' %} - -{% block content %} -

      Edit Page - {{ title }}

      - -
      - - - - -
      - -
      - -
      - - - -
      - Last Updated: {{ rows.updated|date("Y-m-d H:i:s") }}, - Created: {{ rows.created|date("Y-m-d H:i:s") }} -
      -
      - -
      - - - - -
      - {% include '@novacore/editpage/tab-main.html.twig' %} -
      -
      - {% include '@novacore/editpage/tab-metadata.html.twig' %} -
      -
      - {% include '@novacore/editpage/tab-other.html.twig' %} -
      -
      - {% include '@novacore/editpage/tab-tweaks.html.twig' %} -
      -
      - {% include '@novacore/editpage/tab-notes.html.twig' %} -
      -
      - {% include '@novacore/editpage/tab-tags.html.twig' %} -
      -
      - -
      - -{% endblock %} diff --git a/views/editpage/tab-main.html.twig b/views/editpage/tab-main.html.twig deleted file mode 100644 index bf78f91..0000000 --- a/views/editpage/tab-main.html.twig +++ /dev/null @@ -1,29 +0,0 @@ -
      - - -
      - - -
      - -
      - -
      {# Ace mounts here #} -
      -
      - -
      - -
      \ No newline at end of file diff --git a/views/editpage/tab-metadata.html.twig b/views/editpage/tab-metadata.html.twig deleted file mode 100644 index 9e4424e..0000000 --- a/views/editpage/tab-metadata.html.twig +++ /dev/null @@ -1,17 +0,0 @@ - -

      Metadata

      - -
      - - -
      - -
      - - -
      - -
      - - -
      diff --git a/views/editpage/tab-notes.html.twig b/views/editpage/tab-notes.html.twig deleted file mode 100644 index d4a085c..0000000 --- a/views/editpage/tab-notes.html.twig +++ /dev/null @@ -1,4 +0,0 @@ -
      - - -
      \ No newline at end of file diff --git a/views/editpage/tab-other.html.twig b/views/editpage/tab-other.html.twig deleted file mode 100644 index c08cb11..0000000 --- a/views/editpage/tab-other.html.twig +++ /dev/null @@ -1,25 +0,0 @@ -

      Sitemap

      - -
      - - -
      - -
      - - -
      - -
      - - -
      - - \ No newline at end of file diff --git a/views/editpage/tab-tags.html.twig b/views/editpage/tab-tags.html.twig deleted file mode 100644 index 3196e69..0000000 --- a/views/editpage/tab-tags.html.twig +++ /dev/null @@ -1,12 +0,0 @@ -

      Tags

      - -
      -
      - - {# Custom dropdown #} -
      - -
      \ No newline at end of file diff --git a/views/editpage/tab-tweaks.html.twig b/views/editpage/tab-tweaks.html.twig deleted file mode 100644 index 09c3ee1..0000000 --- a/views/editpage/tab-tweaks.html.twig +++ /dev/null @@ -1,10 +0,0 @@ -

      Page Data

      -
      - - -
      - -
      - - -
      \ No newline at end of file diff --git a/views/init.html.twig b/views/init.html.twig deleted file mode 100644 index c2e763e..0000000 --- a/views/init.html.twig +++ /dev/null @@ -1,67 +0,0 @@ -{% extends '@novaconium/master.html.twig' %} - -{% block content %} -

      {{title}}

      - - {% if not secure_key %} -
      -

      Secure Key

      -

      Please set the secure_key in App/config.php to an alphanumeric code that is 64 characters in length.

      -

      You can generate a secure key like this:

      -
      pwgen -sB 64 1
      -

      Or use this one:

      -
      {{gen_key}}
      -
      - {% endif %} - - {% if users_created %} -
      -

      Users Table Created

      -

      There was no users table in the database. One was created.

      -
      - {% endif %} - - {% if empty_users and secure_key %} -
      -

      Create Admin

      -

      No admin users exist, make an admin user now.

      - -
      - - -
      -

      - -
      -

      - -
      -

      - -
      -

      - - -
      -
      - {% endif %} - - {% if show_login %} -
      -

      Administrator Login

      - -
      - - -
      -

      - -
      -

      - - -
      -
      - {% endif %} - -{% endblock %} diff --git a/views/messages.html.twig b/views/messages.html.twig deleted file mode 100644 index 5c4ead2..0000000 --- a/views/messages.html.twig +++ /dev/null @@ -1,43 +0,0 @@ -{% extends '@novaconium/cp/control-panel.html.twig' %} - -{% block content %} -

      {{title}}

      - - - - - - - - - - - - - {% for msg in messages %} - - - - - - - - - {% else %} - - - - {% endfor %} - -
      EmailNameMessage PreviewCreatedStatusActions
      {{ msg.email }}{{ msg.name }}{{ msg.message }}{{ msg.created|date('Y-m-d H:i') }} - {% if msg.unread %} - Unread - {% else %} - Read - {% endif %} - - Edit - Delete -
      No messages found
      - -{% endblock %} diff --git a/views/pages.html.twig b/views/pages.html.twig deleted file mode 100644 index e8c0683..0000000 --- a/views/pages.html.twig +++ /dev/null @@ -1,30 +0,0 @@ -{% extends '@novaconium/cp/control-panel.html.twig' %} - -{% block content %} -

      {{title}}

      -

      Create Page

      - - - - - - - - - - - {% for page in pages %} - - - - - - - {% else %} - - - - {% endfor %} - -
      TitleCreatedUpdatedDraft
      {{ page.title }}{{ page.created }}{{ page.updated|default('Not updated') }}{{ page.draft ? 'Draft' : 'Published' }}
      No pages found.
      -{% endblock %} diff --git a/views/samples/basic.html.twig b/views/samples/basic.html.twig deleted file mode 100644 index 77386a7..0000000 --- a/views/samples/basic.html.twig +++ /dev/null @@ -1,269 +0,0 @@ -{% extends '@novaconium/master.html.twig' %} - -{% set title = "Basic HTML Elements" %} -{% set description = "This is the basic html page from hugo" %} -{% set keywords = "html, css, javascript, php, twig" %} -{% set author = "anonymous" %} - -{% block content %} - -
      -
      -

      {{ title }}

      - -
      -
      -

      The main purpose of this article is to make sure that all basic HTML Elements are decorated with CSS so as to not miss any possible elements when creating new themes for Hugo.

      - -

      Headings

      - -

      Let’s start with all possible headings. The HTML <h1><h6> elements represent six levels of section headings. <h1> is the highest section level and <h6> is the lowest.

      - -

      Heading 1

      - -

      Heading 2

      - -

      Heading 3

      - -

      Heading 4

      - -
      Heading 5
      - -
      Heading 6
      - -
      - -

      Paragraph

      - -

      According to the HTML5 specification by W3C, HTML documents consist of a tree of elements and text. Each element is denoted in the source by a start tag, such as <body>, and an end tag, such as </body>. (Certain start tags and end tags can in certain cases be omitted and are implied by other tags.)

      - -

      Elements can have attributes, which control how the elements work. For example, hyperlink are formed using the a element and its href attribute.

      - -

      List Types

      - -

      Ordered List

      - -
        -
      1. First item
      2. -
      3. Second item
      4. -
      5. Third item
      6. -
      - -

      Unordered List

      - -
        -
      • List item
      • -
      • Another item
      • -
      • And another item
      • -
      - -

      Nested list

      - -
        -
      • First item
      • -
      • Second item -
          -
        • Second item First subitem
        • -
        • Second item second subitem -
            -
          • Second item Second subitem First sub-subitem
          • -
          • Second item Second subitem Second sub-subitem
          • -
          • Second item Second subitem Third sub-subitem
          • -
          -
        • -
        • Second item Third subitem -
            -
          1. Second item Third subitem First sub-subitem
          2. -
          3. Second item Third subitem Second sub-subitem
          4. -
          5. Second item Third subitem Third sub-subitem
          6. -
          -
        -
      • -
      • Third item
      • -
      - -

      Definition List

      - -

      HTML also supports definition lists.

      - -
      -
      Blanco tequila
      -
      The purest form of the blue agave spirit...
      -
      Reposado tequila
      -
      Typically aged in wooden barrels for between two and eleven months...
      -
      - -

      Blockquotes

      - -

      The blockquote element represents content that is quoted from another source, optionally with a citation which must be within a footer or cite element, and optionally with in-line changes such as annotations and abbreviations.

      - -
      -

      Quoted text. -This line is part of the same quote. -Also you can put Markdown into a blockquote.

      -
      - -

      Blockquote with a citation.

      - -
      -

      My goal wasn't to make a ton of money. It was to build good computers. I only started the company when I realized I could be an engineer forever.

      -
      Steve Wozniak
      -
      - -

      According to Mozilla’s website, Firefox 1.0 was released in 2004 and became a big success.

      - -

      Tables

      - -

      Tables aren’t part of the core Markdown spec, but Hugo supports them.

      - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      IDMakeModelYear
      1HondaAccord2009
      2ToyotaCamry2012
      3HyundaiElantra2010
      - -

      Colons can be used to align columns.

      - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
      TablesAreCool
      align: leftalign: centeralign: right
      align: leftalign: centeralign: right
      align: leftalign: centeralign: right
      - -

      You can also use inline Markdown.

      - - - - - - - - - - - - - - - - - - - -
      InlineMarkdownInTable
      italicsboldstrikethroughcode
      - -

      Code

      - -
      <!DOCTYPE html>
      -<html lang="en">
      -<head>
      -  <meta charset="UTF-8">
      -  <title>Example HTML5 Document</title>
      -</head>
      -<body>
      -  <p>Test</p>
      -</body>
      -</html>
      -
      - -
      <!DOCTYPE html>
      -<html lang="en">
      -<head>
      -  <meta charset="UTF-8">
      -  <title>Example HTML5 Document</title>
      -</head>
      -<body>
      -  <p>Test</p>
      -</body>
      -</html>
      - -

      Other stuff — abbr, sub, sup, kbd, etc.

      - -

      GIF is a bitmap image format.

      - -

      H2O

      - -

      C6H12O6

      - -

      Xn + Yn = Zn

      - -

      Press X to win. Or press CTRL+ALT+F to show FPS counter.

      - -

      As a unit of information in information theory, the bit has alternatively been called a shannon, named after Claude Shannon, the founder of field of information theory.

      -
      - -
      - -{% endblock %} diff --git a/views/settings.html.twig b/views/settings.html.twig deleted file mode 100644 index 3f0432d..0000000 --- a/views/settings.html.twig +++ /dev/null @@ -1,6 +0,0 @@ -{% extends '@novaconium/cp/control-panel.html.twig' %} - -{% block content %} -

      {{title}}

      -

      Settings will go here.

      -{% endblock %}