Files
Brad ArantandClaude Sonnet 5 867ace3cb3 Give jetc a dedicated, fully inert <comment> handler at compile time
<comment> had no special-case in jetc's compiler at all - it fell
through to parseFallback(), the generic unknown-tag path, which hunts
for a tag's own closing tag via scanBody()/skipQuotedValue(), the same
quote-aware, backslash-escape-interpreting scanning real Jet attribute
syntax needs. A comment containing an explanatory " sequence (a JSON
example in prose, say) or a literal <else>/<if>-looking word - both
already confirmed, separately, to corrupt jetc when they turn up
inside a JS // comment - hit the exact same corruption here too, just
triggered from inside a <comment> block instead.

Added skipCommentTag(), which only tracks same-tag-name nesting depth
via the existing case-insensitive matchTagOpen/matchTagClose and
otherwise ignores every character - no quote tracking, no other-tag
recognition, nothing a comment's own body could ever trip over. Wired
into parseNodes() alongside the other recognized top-level tags;
unlike those, it pushes no node at all; the comment contributes
nothing to the compiled output, matching the runtime interpreters own
__comment behavior but now safe at compile time too.

Verified directly: a scratch .jet with five <comment> blocks (plain,
a bare quote, a backslash-quote JSON example, literal <else>/<if>
text, and nested comments with their own quotes) all compiled clean
and left zero trace in the generated code, with the <if> immediately
following them compiling correctly too - confirming nothing after a
tricky comment gets corrupted either.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-10-10 16:49:53 +00:00

120 lines
5.4 KiB
C++
Executable File

#ifndef __JetParser_h__
#define __JetParser_h__
#include "JetAST.h"
#include <string>
#include <map>
#include <utility>
namespace jetc {
// Standalone parser for a subset of the JET template grammar. Mirrors the
// character-level grammar implemented by JetCore's Tag::parseContainer /
// Tag::Tag (attribute scanning, same-tag-name nesting depth, the <if>
// <else> split) closely enough for well-formed templates, but is a fresh
// implementation - it does not reuse JetCore's Tag classes, since those
// fuse parsing and execution together (a real parse/execute split there
// is a separate, larger refactor). Anything outside the modeled subset
// (set/if/for/while/$[...]/literal text) is captured as an exact raw
// source span and left for the existing interpreter (Tag::interpretSpan)
// to run at compiled-program runtime, so unsupported constructs degrade
// gracefully instead of failing to compile.
class Parser {
public:
// topLevel is false for any nested sub-parse (an <if>/<for>/<while>
// body, via parseSub) and true for the file's own top-level flow and
// for anything spliced in from an <include> (which inherits the
// includer's own topLevel, since it's meant to read as if written
// inline at that point - see parseInclude). Only used to decide
// whether a <tag name="X"> definition is eligible for compilation
// (see parseTagDef) - one found at topLevel==false (nested inside
// another construct) stays FALLBACK, unchanged, matching __tag.cpp's
// own locally-scoped-registration semantics that only a true
// top-level definition is guaranteed to always be registered by the
// time any invocation could reach it.
explicit Parser(const std::string &src, std::string baseDir = "", int includeDepth = 0, bool topLevel = true)
: s(src), pos(0), baseDir(baseDir), includeDepth(includeDepth), topLevel(topLevel) {}
std::vector<NodePtr> parseNodes();
private:
std::string s;
size_t pos;
size_t nextId = 0;
std::string baseDir;
int includeDepth;
bool topLevel;
bool eof() const { return pos >= s.size(); }
bool starts(const std::string &lit) const;
static bool isNameChar(char c);
void skipWs();
std::string readName();
std::string peekTagName(size_t at) const;
bool matchTagOpen(const std::string &name, size_t at) const;
bool matchTagClose(const std::string &name, size_t at) const;
// p points at '<' of some tag's opening; advances p past its '>' or
// "/>", skipping quoted attribute values. Returns true if self-closing.
bool skipOpenTag(size_t &p);
void skipQuotedValue(size_t &p);
void skipEmbeddedTag(size_t &p);
// <comment>...</comment> must be fully inert regardless of what's
// inside it - unlike the generic parseFallback/scanBody path this
// tag used to fall through to (which applies the same quote/
// backslash-escape and nested-tag scanning real Jet syntax needs),
// this tracks only same-tag-name nesting depth via matchTagOpen/
// matchTagClose and otherwise ignores every character. See its own
// definition for the quote-corruption bug this specifically closes.
void skipCommentTag(size_t &p);
std::map<std::string, std::string> parseAttrs(bool &hasContainer);
// Scans forward from bodyStart (just after the opening tag's '>') for
// tagName's matching close, tracking same-tag-name nesting depth and
// (if lookForElse) a depth==1 "<else>" split point.
// Returns {closePos, elsePos} (elsePos == npos if not found/not sought).
std::pair<size_t, size_t> scanBody(size_t bodyStart, const std::string &tagName, bool lookForElse);
std::vector<NodePtr> parseSub(size_t start, size_t end);
// <include file="..."> is spliced in at compile time - read the
// referenced file (resolved relative to baseDir) and parse ITS
// content as if it had been written inline here, so everything in
// it (custom <tag> definitions included) goes through this real
// compiler, not the separate runtime interpreter (which has its own,
// independently-buggy nested-tag handling). Returns the parsed nodes
// to be spliced into the caller's own node list.
std::vector<NodePtr> parseInclude();
NodePtr parseVar();
NodePtr parseSet();
NodePtr parseIf();
NodePtr parseFor();
NodePtr parseWhile();
NodePtr parseFallback();
// <tag name="X">...</tag> at true top level (see topLevel above) -
// compiles the definition body with a nested (topLevel=false) parse
// instead of falling back to raw text, so jetc can generate a real
// render function for it (see JetCodegen). Falls back to an ordinary
// FALLBACK node (same as today) if the definition has no container
// or no literal name= - degrades gracefully rather than failing to
// compile.
NodePtr parseTagDef();
// <container> (any case/attributes/closing style, always void - see
// isVoidTag) - a leaf node; the actual rendering logic lives in
// Tag::renderContainer, called from generated code exactly as the
// interpreter's own <container> dispatch calls it today.
NodePtr parseContainerSlot();
std::string newId(const std::string &prefix) { return prefix + std::to_string(nextId++) + "_"; }
};
}
#endif