Scope custom <tag> definitions to their enclosing invocation - and fix
two real, previously-latent bugs found while building this Custom tag definitions used to be entirely flat: every <tag name="..."> - regardless of where in the script it appeared - registered into a single global.tags map, visible everywhere for the rest of the script. Now a <tag> defined directly inside another custom tag's own invocation (i.e. nested inside a __dotag instance's own expansion, not just sitting inside an <if>/<for>/<while> that happens to be there - those remain transparent) is scoped to that invocation: visible only for the rest of that specific expansion, gone once it finishes. Definitions at the true top level (or inside control-flow tags that are themselves at the top level) still register globally, unchanged. Implementation: Tag gained isCustomTagBoundary (true only on __dotag instances) and localTags (populated only on boundary instances) - both public rather than protected, since __tag.cpp needs to read/write them through an arbitrary ancestor Tag* found by walking parent, and C++ protected access doesn't extend to another object reached through a base-typed pointer. findTagDefinition() walks from `this` up through `parent` looking for the nearest boundary whose localTags has the name, falling back to global.tags - used by both ifTagDefined() (is <name> visible right here?) and __dotag's own constructor (fetch the definition to expand). __tag.cpp's registration does the mirror walk starting from `parent` (skip self, a <tag> definition is never itself a boundary). Two real bugs found and fixed along the way, neither introduced by this feature, both confirmed via a hung/broken real jet-2.0 run before diagnosis rather than assumed: 1. __tag.cpp's own base Tag(...) call passed (this, this) as its own parent/local instead of the parent/local it actually received - made the new scope-walk loop on itself forever (since <tag>'s own `parent` would just point at itself). Fixed to pass through the real received parent/local; nothing else in this class depended on the old self-referential values (a <tag> definition's own container is never evaluated at definition time - evaluate=false - so parent/local were never used for anything else here). 2. __dotag never overrode hasContainer, which Tag::processContainer gates real rendering on - and the base constructor sets hasContainer from whether THIS INVOCATION was self-closing (<name /> vs <name>...</name>), which is the right question for capturing an invocation's own nested content, but the wrong one for whether the tag's OWN DEFINITION should render. Net effect, confirmed via a real run: <name /> always silently rendered nothing for ANY custom tag - only the <name></name> form ever worked. Fixed by forcing hasContainer=true in __dotag's constructor - by the time it's constructed a valid definition is guaranteed (dispatch only builds __dotag after ifTagDefined() already confirmed one exists), so there's always something to render regardless of invocation syntax. <dump> updated to match: its "DEFINED TAGS" section now renders the real tree currently in effect (global at the root, each enclosing custom-tag invocation nested one level deeper, outermost first) instead of a single flat list - if there's no active local scope, that's just the root with nothing nested under it. Verified via real jet-2.0 runs, not just compilation: a tag defined inside another tag's body is invokable from within that body and correctly NOT recognized (falls through to literal-text echo) outside it; the same holds through 3 levels of nesting; the dump tree correctly shows nested levels indented under their own "(inside X)" header. Full regression pass on <try>/<catch>, <if>/<else>, <while>, and dump's own flat top-level output - all still work unchanged.
This commit is contained in:
1 parent
a7dab09150
commit
8483618012
5 files changed
+127
-13
No files matched your search
@@ -318,11 +318,28 @@ namespace jet {
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Tag::findTagDefinition(coreutils::MString name, coreutils::ZString &result) {
|
||||
Tag *scope = this;
|
||||
while(scope) {
|
||||
if(scope->isCustomTagBoundary && scope->localTags.count(name)) {
|
||||
result = scope->localTags[name];
|
||||
return true;
|
||||
}
|
||||
scope = scope->parent;
|
||||
}
|
||||
if(global.tags.count(name)) {
|
||||
result = global.tags[name];
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Tag::ifTagDefined(coreutils::ZString &in, coreutils::ZString &tag) {
|
||||
in.push();
|
||||
if(in.ifNext("<")) {
|
||||
tag = in.getTokenInclude("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_");
|
||||
if(global.tags.count(tag)) {
|
||||
coreutils::ZString definition;
|
||||
if(findTagDefinition(tag, definition)) {
|
||||
if(in.ifNext(" ")) {
|
||||
in.pop();
|
||||
return true;
|
||||
@@ -332,10 +349,10 @@ namespace jet {
|
||||
} else if(in.ifNext(">")) {
|
||||
in.pop();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
in.pop();
|
||||
}
|
||||
}
|
||||
}
|
||||
in.pop();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -46,6 +46,32 @@ namespace jet {
|
||||
// the 22 tag classes' logic.
|
||||
void interpretSpan(coreutils::ZString span, coreutils::MString &out);
|
||||
|
||||
// Custom <tag name="..."> scoping: a Tag defined directly inside
|
||||
// another custom tag's own invocation (i.e. inside a __dotag
|
||||
// instance's expansion, not just nested inside <if>/<for>/etc. that
|
||||
// happen to sit there) registers into that invocation's own
|
||||
// localTags instead of the single flat global.tags map, so it's
|
||||
// only visible for the rest of that invocation - not the whole
|
||||
// script. isCustomTagBoundary is true only on __dotag instances
|
||||
// (set in __dotag's own constructor body); every other tag leaves
|
||||
// it false and is transparently walked past when searching for an
|
||||
// enclosing scope, exactly like <if>/<for>/<while> already are for
|
||||
// parent-chain purposes elsewhere. Public (like variables/keywords/
|
||||
// name/etc. above) rather than protected - __tag.cpp needs to read
|
||||
// and write these through an arbitrary ancestor Tag* found by
|
||||
// walking parent, not just on its own `this`, and protected access
|
||||
// in C++ only extends to a derived class's own (sub)object, not to
|
||||
// another object reached through a base-class-typed pointer.
|
||||
bool isCustomTagBoundary = false;
|
||||
std::map<coreutils::MString, coreutils::MString> localTags;
|
||||
|
||||
// Walks from `this` up through `parent` looking for the nearest
|
||||
// isCustomTagBoundary ancestor whose localTags has `name`, falling
|
||||
// back to global.tags - used by both ifTagDefined() (is <name>
|
||||
// visible right here?) and __dotag's own constructor (fetch the
|
||||
// actual definition to expand).
|
||||
bool findTagDefinition(coreutils::MString name, coreutils::ZString &result);
|
||||
|
||||
protected:
|
||||
bool hasContainer = false;
|
||||
bool hasContainer2 = false;
|
||||
|
||||
+28
-2
@@ -2,9 +2,35 @@
|
||||
#include "Exception.h"
|
||||
|
||||
namespace jet {
|
||||
|
||||
|
||||
__dotag::__dotag(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local) : Tag(in, parentOut, global, parent, local) {
|
||||
coreutils::ZString container3 = global.tags[name];
|
||||
// Every invocation of a custom tag is itself a new scope boundary -
|
||||
// a <tag name="..."> encountered while expanding this invocation's
|
||||
// own template (below) registers into THIS instance's localTags,
|
||||
// not the global map, so it's only visible for the rest of this
|
||||
// expansion. See Tag.h's isCustomTagBoundary/localTags doc comment.
|
||||
isCustomTagBoundary = true;
|
||||
|
||||
// The base Tag constructor already set hasContainer from whether
|
||||
// THIS INVOCATION was self-closing (<name /> vs <name>...</name>) -
|
||||
// that's the right question for capturing this invocation's own
|
||||
// nested content into `container` above, but the wrong one for
|
||||
// whether the tag's DEFINITION (container3 below) should render:
|
||||
// a self-closing invocation of a tag whose definition doesn't use
|
||||
// <container /> at all is completely normal and should still
|
||||
// render. Found this as a real, pre-existing bug while testing
|
||||
// this scoping feature - <name /> always silently rendered nothing
|
||||
// for ANY custom tag, only <name></name> worked, since
|
||||
// processContainer() gates on hasContainer. Force it true here:
|
||||
// by this point a valid definition is guaranteed to exist (dispatch
|
||||
// only constructs __dotag after ifTagDefined() already confirmed
|
||||
// it), so there's always something to render regardless of how
|
||||
// this invocation was written.
|
||||
hasContainer = true;
|
||||
|
||||
coreutils::ZString container3;
|
||||
if(!findTagDefinition(name, container3))
|
||||
throw coreutils::Exception("tag definition not found.");
|
||||
processContainer(container3, container);
|
||||
}
|
||||
}
|
||||
|
||||
+29
-6
@@ -2,6 +2,7 @@
|
||||
#include "Exception.h"
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <vector>
|
||||
|
||||
namespace jet {
|
||||
|
||||
@@ -33,14 +34,36 @@ namespace jet {
|
||||
for (auto i = global.cookies.data.begin(); i != global.cookies.data.end(); i++)
|
||||
outFile << i->first << "=[" << i->second << "]" << std::endl;
|
||||
|
||||
// Unlike variables (which have real global/local/parent scoping),
|
||||
// custom <tag name="..."> definitions have only one level - __tag.cpp
|
||||
// always registers into global.tags regardless of where in the
|
||||
// script the definition sits, so there's nothing to break out by
|
||||
// scope here.
|
||||
// Custom <tag name="..."> definitions now have real scoping - one
|
||||
// defined directly inside another custom tag's own invocation is
|
||||
// only visible for the rest of that invocation (see Tag.h's
|
||||
// isCustomTagBoundary/localTags doc comment). Render the actual
|
||||
// tree currently in effect at this point in the script: global at
|
||||
// the root, then each enclosing custom-tag invocation nested one
|
||||
// level deeper, outermost first - if there's no active local scope
|
||||
// (this <dump> isn't running inside any custom tag's expansion),
|
||||
// that's just the root with nothing nested under it.
|
||||
outFile << "*** DEFINED TAGS ***" << std::endl;
|
||||
outFile << "(global)" << std::endl;
|
||||
for (auto i = global.tags.begin(); i != global.tags.end(); i++)
|
||||
outFile << i->first << std::endl;
|
||||
outFile << " " << i->first << std::endl;
|
||||
|
||||
std::vector<Tag *> scopeChain;
|
||||
for (Tag *scope = this; scope; scope = scope->parent)
|
||||
if (scope->isCustomTagBoundary)
|
||||
scopeChain.push_back(scope);
|
||||
|
||||
int depth = 1;
|
||||
for (auto it = scopeChain.rbegin(); it != scopeChain.rend(); ++it) {
|
||||
Tag *boundary = *it;
|
||||
coreutils::MString indent;
|
||||
for (int d = 0; d < depth; d++)
|
||||
indent << " ";
|
||||
outFile << indent << "(inside " << boundary->name << ")" << std::endl;
|
||||
for (auto i = boundary->localTags.begin(); i != boundary->localTags.end(); i++)
|
||||
outFile << indent << " " << i->first << std::endl;
|
||||
depth++;
|
||||
}
|
||||
|
||||
outFile.close();
|
||||
|
||||
|
||||
@@ -3,14 +3,36 @@
|
||||
|
||||
namespace jet {
|
||||
|
||||
__tag::__tag(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local) : Tag(in, parentOut, global, this, this) {
|
||||
__tag::__tag(coreutils::ZString &in, coreutils::MString &parentOut, Global &global, Tag *parent, Tag *local) : Tag(in, parentOut, global, parent, local) {
|
||||
evaluate = false;
|
||||
output = false;
|
||||
if(!keywordDefined("name"))
|
||||
throw coreutils::Exception("tag must have a name.");
|
||||
if(!hasContainer)
|
||||
throw coreutils::Exception("tag requires a container to process.");
|
||||
global.tags[keywords["name"]] = container;
|
||||
|
||||
// A <tag> defined directly inside another custom tag's own
|
||||
// invocation (a __dotag instance's expansion) is scoped to that
|
||||
// invocation - only visible for the rest of it, not the whole
|
||||
// script. Control-flow tags like <if>/<for> that happen to sit in
|
||||
// between are transparent (walked past, not boundaries themselves).
|
||||
// Reaching parent==NULL (true top level) falls back to the original
|
||||
// single flat global.tags map, unchanged from before. This relies
|
||||
// on `parent` here actually being the real enclosing tag - the base
|
||||
// Tag(...) call above used to pass (this, this) instead of the
|
||||
// parent/local this constructor actually received, which would have
|
||||
// made this walk loop on itself forever; fixed as part of adding
|
||||
// this scoping (parent/local weren't needed anywhere else in this
|
||||
// class - <tag>'s own container is never evaluated at definition
|
||||
// time, evaluate=false above, so nothing depended on the old
|
||||
// self-referential values).
|
||||
Tag *scope = parent;
|
||||
while(scope && !scope->isCustomTagBoundary)
|
||||
scope = scope->parent;
|
||||
if(scope)
|
||||
scope->localTags[keywords["name"]] = container;
|
||||
else
|
||||
global.tags[keywords["name"]] = container;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user