@yr-lang/yr 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/BUGS.md +3 -21
  2. package/CHANGELOG.md +45 -0
  3. package/package.json +2 -2
  4. package/yr.js +70 -43
package/BUGS.md CHANGED
@@ -3,21 +3,10 @@
3
3
  This file documents known bugs and limitations in yr.
4
4
  The project is under active development.
5
5
 
6
- ### Wrapper-only section closure can fail
6
+ ### Wrapper-only section closure Fixed in 0.2.0
7
7
 
8
- In some cases, a section cannot be closed using only a wrapper, and must instead be closed explicitly with an element (_).
9
-
10
- For example, the following may result in an error:
11
-
12
- ```
13
- ++
14
- _ .teste
15
- _wrapper/test
16
- _ .teste2
17
- _wrapper/test2
18
- ```
19
-
20
- However, explicitly closing the section with elements works as expected:
8
+ Previously, a section could not always be closed using only a wrapper and required an explicit
9
+ `_` element. This has been resolved — the following now works as expected:
21
10
 
22
11
  ```
23
12
  ++
@@ -25,11 +14,4 @@ _ .teste
25
14
  _wrapper/test
26
15
  _ .teste2
27
16
  _wrapper/test2
28
- _
29
- _
30
17
  ```
31
-
32
- #### Notes
33
-
34
- * This appears to be related to how section termination is detected when the last node is a wrapper.
35
- * The parser may not recognize wrappers as valid closing tokens in some nesting scenarios.
package/CHANGELOG.md ADDED
@@ -0,0 +1,45 @@
1
+ # Changelog
2
+
3
+ ## [0.2.0] - 2026-03-01
4
+
5
+ ### Fixed
6
+
7
+ - **Wrapper-only section closure** — Sections containing nested wrappers no longer require an
8
+ explicit `_` to close. The parser now correctly closes open layers when a wrapper is terminated
9
+ by indentation, producing balanced HTML in all nesting scenarios. Resolves the issue documented
10
+ in `BUGS.md`.
11
+
12
+ - **Multiple wrapper closure on a single line** — When an element's indentation requires closing
13
+ more than one wrapper at once, the parser now closes all of them (changed `if` to `while` in
14
+ the html parser wrapper check). Previously only the innermost wrapper was closed, causing
15
+ sibling elements to be rendered inside the wrong wrapper.
16
+
17
+ - **Section change leaves wrappers open** — When a namespace token (e.g. `><` → `##`) was
18
+ encountered while one or more wrappers were active, only the outermost wrapper was cleaned up
19
+ via `shift()`. Now all open wrappers are properly closed innermost-first.
20
+
21
+ - **Closing tag indentation** — `getWhiteSpace()` was designed to receive a hierarchy level but
22
+ was called with a raw character-count indentation, producing increasingly wrong whitespace for
23
+ deeper elements. Closing tags now mirror the indentation of their opening tags.
24
+
25
+ - **`throw -1` loses file/project context** — A stray semicolon caused the error message for
26
+ odd-indentation errors to drop the filename and project name. The `-1` error message now
27
+ matches the `-2` error message in completeness.
28
+
29
+ - **`getElementAttributes` spurious empty key** — When an `att={{}}` block ended with a trailing
30
+ comma, an empty string key was unconditionally added to the parsed attributes object. Now only
31
+ added if `parseKey` is non-empty.
32
+
33
+ - **Self-importing wrapper causes infinite recursion** — A wrapper that listed itself via `!!`
34
+ in its own `++` section would loop indefinitely. The extension is now registered in
35
+ `sections.extensions` before its content is parsed, so the guard fires correctly on
36
+ re-entry.
37
+
38
+ - **Wrapper without `++` section does not invoke JS function** — A wrapper that had a
39
+ `_@wrapper(Category, Option)` function in `@>` but no `++` body would have its div created
40
+ but the JS function never called. The `wrapperjs` entry is now added independently of whether
41
+ a `++` section exists.
42
+
43
+ ## [0.1.1] - prior
44
+
45
+ Initial public releases.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yr-lang/yr",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "description": "Yr is a modern structural language and parser",
5
5
  "main": "./node.js",
6
6
  "exports": {
@@ -9,6 +9,6 @@
9
9
  "license": "SEE LICENSE IN LICENSE",
10
10
  "repository": {
11
11
  "type": "git",
12
- "url": "https://github.com/yr-lang/yr"
12
+ "url": "git+https://github.com/yr-lang/yr.git"
13
13
  }
14
14
  }
package/yr.js CHANGED
@@ -17,10 +17,11 @@ const utils = {
17
17
  };
18
18
 
19
19
  const common = {
20
+ // Bug fix: was using new Array((i*2)-1).join which was designed for 'level' units,
21
+ // but is called with raw indentation (char count). Use simple repeat instead.
20
22
  getWhiteSpace(indentation) {
21
- try {
22
- return new Array((indentation * 2) - 1).join(' ');
23
- } catch(error) { return ''; }
23
+ if (!indentation || indentation < 0) return '';
24
+ return ' '.repeat(indentation);
24
25
  }
25
26
  };
26
27
 
@@ -140,7 +141,9 @@ function getElementAttributes(line) {
140
141
  lastChar = value;
141
142
  }
142
143
 
143
- parse[parseKey.trim()] = parseValue.trim();
144
+ // Bug fix: only add the last key-value if parseKey is non-empty
145
+ // (avoids adding a spurious empty key when attrs end with a trailing comma)
146
+ if (parseKey.trim()) parse[parseKey.trim()] = parseValue.trim();
144
147
  parseKey = '', parseValue = '';
145
148
  return { ...attributes, ...parse };
146
149
  }
@@ -234,16 +237,32 @@ const parserFns = {
234
237
  }
235
238
  yrParsedLine = addIdToElement(yrParsedLine, config, elementId);
236
239
  let yrIndentation = 0, ignoreYr, wildCard = '', replaceYr;
237
- if (state.wrapper.length > 0) {
240
+ // Bug fix: use while loop to close ALL wrappers that need closing (not just one)
241
+ // A single element may need to close multiple nested wrappers at once
242
+ while (state.wrapper.length > 0) {
238
243
  let wrapper = state.wrapper[state.wrapper.length - 1];
239
244
  if (parsedLine.indentation >= wrapper.reference) {
240
245
  yrIndentation = wrapper.new - wrapper.reference + 2;
241
246
  replaceYr = sections.yr[section].includes('#@#\n');
247
+ break; // Still inside this wrapper, stop closing
242
248
  } else {
243
- wrapper = state.wrapper[state.wrapper.length - 1];
244
249
  sections.yr[section] = sections.yr[section].replace(/#@#\n/, '');
250
+ // Close any layers that were opened inside this wrapper before popping it
251
+ for (let j = state.layers.length - 1; j >= 0; j--) {
252
+ const layer = state.layers[j];
253
+ if (!layer) continue;
254
+ // Only close layers that belong inside this wrapper (indentation >= wrapper.reference)
255
+ if (layer.indentation >= wrapper.reference) {
256
+ sections[layer.section] +=
257
+ layer.whiteSpace + `</${layer.tag}>\n`;
258
+ state.layers.pop();
259
+ } else {
260
+ break;
261
+ }
262
+ }
245
263
  if (wrapper.wildCard) sections[section] += wrapper.wildCard + '\n';
246
264
  state.wrapper.pop();
265
+ // After popping, loop again to check if we need to close more wrappers
247
266
  }
248
267
  }
249
268
  if (yrIndentation < 0) yrIndentation = 0;
@@ -257,7 +276,7 @@ const parserFns = {
257
276
  }
258
277
  if (parsedLine.indentation !== -1) {
259
278
  if (parsedLine.indentation % 2 !== 0)
260
- throw `-1: indentation error at line ${lineNumber}` + '';
279
+ throw `-1: indentation error at line ${lineNumber}` + '' +
261
280
  ` (${config.name}.yr, ${config.project}):\n\n"""\n${line}\n"""`;
262
281
  if (parsedLine.level > state.level + 1)
263
282
  throw `-2: indentation error at line ${lineNumber}` + '' +
@@ -333,33 +352,28 @@ const parserFns = {
333
352
  state.wrapper.push(newWrapper);
334
353
  wrapperIndentation = parsedLine.indentation;
335
354
  }
355
+ // Always set up element attributes for wrapperjs call,
356
+ // even if wrapper has no ++ section (no wrapperbody)
357
+ elementAttributes.id = elementId;
358
+ elementAttributes.category = wrapper[0];
359
+ elementAttributes.option = wrapper[1];
360
+ let attributes = [];
361
+ try {
362
+ attributes = sections.wrappers[wrapperName]
363
+ ? sections.wrappers[wrapperName].vars.attributes
364
+ : sections.vars.attributes;
365
+ } catch(error) {/* pass */}
366
+ if (!attributes) attributes = sections.vars.attributes;
367
+ elementAttributes.attributes = attributes;
368
+ // Call the JS wrapper function if it exists, regardless of whether ++ is present
369
+ if (sections.jsheader
370
+ .includes(`function __${wrapperName.replace(/\//, '_')}(`)
371
+ && !sections.wrapperjs.includes(`"id":"${elementId}"`)) {
372
+ const wrapperjs =
373
+ `__${wrapperName.replace(/\//, '_')}(${JSON.stringify(elementAttributes)});\n`;
374
+ sections.wrapperjs += wrapperjs;
375
+ }
336
376
  if (sections.wrappers[wrapperName]) {
337
- elementAttributes.id = elementId;
338
- elementAttributes.category = wrapper[0];
339
- elementAttributes.option = wrapper[1];
340
- let attributes = [];
341
- try {
342
- attributes = sections.wrappers[wrapperName].vars.attributes;
343
- } catch(error) {
344
- console.log(wrapperName);
345
- console.log(sections.wrappers[wrapperName]);
346
- console.log(parsedLine);
347
- console.log(error);
348
- throw error;
349
- }
350
- if (!attributes) attributes = sections.vars.attributes;
351
- elementAttributes.attributes = attributes;
352
- let redone;
353
- try {
354
- redone = !sections.wrappers[wrapperName].redone;
355
- } catch(error) {/* pass */}
356
- if (sections.jsheader
357
- .includes(`function __${wrapperName.replace(/\//, '_')}(`)
358
- && !sections.wrapperjs.includes(`"id":"${elementId}"`)) {
359
- const wrapperjs =
360
- `__${wrapperName.replace(/\//, '_')}(${JSON.stringify(elementAttributes)});\n`;
361
- sections.wrapperjs += wrapperjs;
362
- }
363
377
  if (!tag.includes('!')) {
364
378
  if (sections.wrappers[wrapperName].yrwrapperbody) {
365
379
  for (let value of sections.wrappers[wrapperName]
@@ -627,6 +641,10 @@ const core = {
627
641
  return;
628
642
  }
629
643
 
644
+ // Bug fix: register extension BEFORE parsing its content
645
+ // to prevent infinite recursion when a wrapper imports itself via !!
646
+ sections.extensions += '!! ' + wrapperName + '\n';
647
+
630
648
  const _yr = sections.yr;
631
649
  sections.yr = {};
632
650
 
@@ -640,8 +658,6 @@ const core = {
640
658
  sections, wrapper: `${wrapper.join('/')}`, ...config
641
659
  });
642
660
 
643
- sections.extensions += '!! ' + wrapperName + '\n';
644
-
645
661
  if (!sections.wrappers[wrapperName])
646
662
  sections.wrappers[wrapperName] = {};
647
663
 
@@ -715,16 +731,25 @@ const core = {
715
731
  section = parsers.namespaces[line].name;
716
732
  state.sectionChanged = true;
717
733
 
718
- if (state.wrapper.length > 0) {
719
- const wrapper = state.wrapper[0];
734
+ // Bug fix: close ALL open wrappers when section changes, innermost first (pop, not shift)
735
+ while (state.wrapper.length > 0) {
736
+ const wrapper = state.wrapper[state.wrapper.length - 1];
720
737
 
721
738
  sections.yr[wrapper.section] =
722
739
  sections.yr[wrapper.section].replace(/#@#\n/, '');
723
740
 
741
+ // Close layers created inside this wrapper
742
+ for (let j = state.layers.length - 1; j >= 0; j--) {
743
+ const layer = state.layers[j];
744
+ if (!layer || layer.indentation < wrapper.reference) break;
745
+ sections[layer.section] += layer.whiteSpace + `</${layer.tag}>\n`;
746
+ state.layers.pop();
747
+ }
748
+
724
749
  if (wrapper.wildCard)
725
750
  sections[wrapper.section] += wrapper.wildCard + '\n';
726
751
 
727
- state.wrapper.shift();
752
+ state.wrapper.pop();
728
753
  }
729
754
 
730
755
  continue;
@@ -861,15 +886,17 @@ const core = {
861
886
  sections[wrapper.section] += wrapper.wildCard + '\n';
862
887
 
863
888
 
864
- for (let j = wrapper.layers.length - 1; j >= 0; j--) {
865
- const layer = wrapper.layers[j];
889
+ // Only close layers that were created INSIDE this wrapper (indentation >= wrapper.reference)
890
+ // External layers (pre-existing before the wrapper) should NOT be closed here
891
+ for (let j = state.layers.length - 1; j >= 0; j--) {
892
+ const layer = state.layers[j];
866
893
  if (!layer) continue;
894
+ if (layer.indentation < wrapper.reference) break;
867
895
 
868
896
  sections[layer.section] +=
869
- common.getWhiteSpace(layer.indentation) + `</${layer.tag}>\n`;
897
+ layer.whiteSpace + `</${layer.tag}>\n`;
870
898
 
871
- wrapper.layers.pop();
872
- state.layers = state.layers.filter(e => e !== layer);
899
+ state.layers.pop();
873
900
  }
874
901
 
875
902
  state.wrapper.pop();