open-xml-templating/docxtemplater

Parser example to avoid using the parent scope if a value is null on the main scope : onlyDeepestScope bug

nestorps opened this issue · 11 comments

Environment

  • Version of docxtemplater : 3.37.11
  • Used docxtemplater-modules : Free module, docxtemplater-image-module-free 1.1.1
  • Runner : TypeScript

Hi, I've been struggling with this code in the documentation:

https://docxtemplater.com/docs/configuration/#parser-example-to-avoid-using-the-parent-scope-if-a-value-is-null-on-the-main-scope

How to reproduce my problem :

this is the code:

const docTmp = new docxtemplater().loadZip(zippedTemplate).setOptions({
    parser: (tag: any) => {
      return {
        get(scope: any, context: any) {
          const onlyDeepestScope = tag[0] === '!';
          if (onlyDeepestScope) {
            if (context.num < context.scopePath.length) {
              return null;
            } else {
              // Remove the leading "!", ie: "!name" => "name"
              tag = tag.substr(1);
            }
          }
          // You can customize the rest of your parser here instead of
          // scope[tag], by using the angular-parser for example.
          return scope[tag];
        },
      };
    },
    nullGetter: (tag: any, props: any) => {
      return '';
    },
    linebreaks: true,
    paragraphLoop: true,
  });

My word template looks like this:

{#entities}Títol de l’exposició: {#activity}{title}
Lloc de l’exposició: {place}
Dates de l’exposició: {dateSince} - {dateUntil}{/activity}
 
{#objects}{#object}{display}
{materialsTechsDisplay}
{measurementsDisplay}
{currentLocation.label}
{creditLine}
Valor assegurança actual: {#valuationAmount}{valuationAmount} EUR {/valuationAmount}	 
{#portraitMedia}**{!original}**{/portraitMedia}
{/object}{/objects}
{/entities}

When portraitMedia is not present in objects/object it always returning the portraitMedia original field from entities scope.
I'm sure there's no portraitMedia property inside some elements on objects/object array.

removing the question mark in the template and changing the parser without condition it works:

 parser: (tag: any) => {
      return {
       get(scope, context) {
            if (context.num < context.scopePath.length) {
                return null;
            }
            // You can customize your parser here instead of scope[tag] of course
            return scope[tag];
        },
      };
    },

Thanks in advance

First possibility

If you want to set the scope to never traverse, then you can use

 parser: (tag: any) => {
      return {
       get(scope, context) {
            if (context.num < context.scopePath.length) {
                return null;
            }
            // You can customize your parser here instead of scope[tag] of course
            return scope[tag];
        },
      };
    },

In that case you can remove the "!" prefix.

Second possibility

If you use following code, it will traverse except if the tag starts with "!"

const docTmp = new docxtemplater().loadZip(zippedTemplate).setOptions({
    parser: (tag: any) => {
      return {
        get(scope: any, context: any) {
          const onlyDeepestScope = tag[0] === '!';
          if (onlyDeepestScope) {
            if (context.num < context.scopePath.length) {
              return null;
            } else {
              // Remove the leading "!", ie: "!name" => "name"
              tag = tag.substr(1);
            }
          }
          // You can customize the rest of your parser here instead of
          // scope[tag], by using the angular-parser for example.
          return scope[tag];
        },
      };
    },
    nullGetter: (tag: any, props: any) => {
      return '';
    },
    linebreaks: true,
    paragraphLoop: true,
  });

In that case, your template should be like this if you don't want to let the scope traverse on the portraitMedia

{#entities}Títol de l’exposició: {#activity}{title}
Lloc de l’exposició: {place}
Dates de l’exposició: {dateSince} - {dateUntil}{/activity}
 
{#objects}{#object}{display}
{materialsTechsDisplay}
{measurementsDisplay}
{currentLocation.label}
{creditLine}
Valor assegurança actual: {#valuationAmount}{valuationAmount} EUR {/valuationAmount}	 
{#!portraitMedia}**{!original}**{/!portraitMedia}
{/object}{/objects}
{/entities}

Thanks for the quick response. Try to add the ! to portraitMedia with the second posibility with no luck:

{#!portraitMedia}{!original}{/!portraitMedia}

still showing the portraitMedia.original from entities

any ideas, thanks in advance

Can you send the data that you're using ?

By the way, I recommend to use the v4 api, that is :

new docxtemplater(zippedTemplate, {
    parser: (tag: any) => {
      return {
        get(scope: any, context: any) {
          const onlyDeepestScope = tag[0] === '!';
          if (onlyDeepestScope) {
            if (context.num < context.scopePath.length) {
              return null;
            } else {
              // Remove the leading "!", ie: "!name" => "name"
              tag = tag.substr(1);
            }
          }
          // You can customize the rest of your parser here instead of
          // scope[tag], by using the angular-parser for example.
          return scope[tag];
        },
      };
    },
    nullGetter: (tag: any, props: any) => {
      return '';
    },
    linebreaks: true,
    paragraphLoop: true,
  });

Here is a diff of some test files where I have made the test work

diff --git a/es6/tests/e2e/fixtures.js b/es6/tests/e2e/fixtures.js
index 0582cf30..bea56494 100644
--- a/es6/tests/e2e/fixtures.js
+++ b/es6/tests/e2e/fixtures.js
@@ -132,6 +132,99 @@ const fixtures = [
 			endText,
 		],
 	},
+	{
+		content: `<w:t>
+		{#entities}
+			Títol de l’exposició:
+			{#activity}
+			{title} Lloc de l’exposició: {place}
+			Dates de l’exposició: {dateSince} - {dateUntil}
+			{/activity}
+
+			{#objects}
+			{#object}
+			{display}
+			{materialsTechsDisplay}
+			{measurementsDisplay}
+			{currentLocation.label}
+			{creditLine}
+			Valor assegurança actual: {#valuationAmount}{valuationAmount} EUR {/valuationAmount}
+			{#!portraitMedia}**{!original}**{/!portraitMedia}
+			{/object}{/objects}
+		{/entities}
+		</w:t>`,
+		options: {
+			parser: (tag) => {
+				return {
+					get(scope, context) {
+						const onlyDeepestScope = tag[0] === "!";
+						if (onlyDeepestScope) {
+							if (context.num < context.scopePath.length) {
+								return null;
+							} else {
+								// Remove the leading "!", ie: "!name" => "name"
+								tag = tag.substr(1);
+							}
+						}
+						// You can customize the rest of your parser here instead of
+						// scope[tag], by using the angular-parser for example.
+						return scope[tag];
+					},
+				};
+			},
+			nullGetter: (tag, props) => {
+				return "";
+			},
+			linebreaks: true,
+			paragraphLoop: true,
+		},
+		scope: {
+			entities: [
+				{
+					activity: true,
+					title: "John",
+					place: "New York",
+					dateSince: "2023-06-11",
+					dateUntil: "2023-08-11",
+					objects: [
+						{
+							object: {
+								display: "hi",
+								portraitMedia: [
+									{
+										original: "YES",
+									},
+								],
+							},
+						},
+					],
+				},
+			],
+		},
+		result: '<w:t xml:space="preserve">Hi Foo</w:t>',
+		postparsed: [
+			xmlSpacePreserveTag,
+			content("Hi "),
+			{ type: "placeholder", value: "." },
+			endText,
+		],
+		only: true,
+		it: "should do #721",
+		lexed: [
+			startText,
+			content("Hi "),
+			delimiters.start,
+			content("."),
+			delimiters.end,
+			endText,
+		],
+		parsed: [
+			startText,
+			content("Hi "),
+			{ type: "placeholder", value: "." },
+			endText,
+		],
+	},
 	{
 		it: "should handle {.} with tag",
 		content: "<w:t>Hi {.}</w:t>",

Hello @nestorps ,

Your JSON is too complex, I can't tell easily what the expected output should be and what you currently have as the output.

Please simplify your template and data to come up with an issue that is reproducible and simple enough to understand.

Hi, I tried to reduce json to the significant fields:

{
  entities: [
    {
      mainTask: {
        referenceNumber: "T-000030",
        label: "[T-000030 / Moviment]",
        
      },
      activity: {
        event: {
          label: "Europa i l'art, 1-3-2019 - 5-8-2019"
        },
        title: "Europa i l'art",
      },
      objects: [
        {
          display: "prova 0001234142 - cable / 0 €",
          object: {
            objectNumber: "aa.0123834",
            portraitMedia: {
              original: "https://coeli-staging-eu-macba-test.s3-eu-west-1.amazonaws.com/private/DigitalAsset/0bcce599-a496-48ec-b829-80e3faf421d6/cd460cf6873e2f6b4d8a706df5cfe034/full/original/0/default.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20230613T134252Z&X-Amz-SignedHeaders=host&X-Amz-Expires=3600&X-Amz-Credential=AKIAINEAG7AQJT6DQBMQ%2F20230613%2Feu-west-1%2Fs3%2Faws4_request&X-Amz-Signature=712b4b98fa8939192458ff7bcc2cb9118aeb4b8e9ed68fcf6f2bbfb44d8c8bdc",
              
            },
            
          },
          
        },
        {
          display: "CF2953.001 - Wind [videocasset], Joan Jonas, 1968",
          object: {
            objectNumber: "CF2953.001",
            portraitMedia: {
              original: "https://coeli-staging-eu-macba-test.s3-eu-west-1.amazonaws.com/uploads/DigitalAsset/7603f97bfd2ebd7eb13eb0fad48fe3fe.jpg",
              
            },
            
          },
          
        },
        {
          display: "A0001.0004 - sobre, 2023 a",
          object: {
            objectNumber: "A0001.0004",
            
          },
          
        },
        {
          display: "A.JBR.00002 / 5 €",
          object: {
            objectNumber: "A.JBR.00002",
            
          },
          
        }
      ],
      portraitMedia: {
        original: "https://coeli-staging-eu-macba-test.s3-eu-west-1.amazonaws.com/private/DigitalAsset/0bcce599-a496-48ec-b829-80e3faf421d6/cd460cf6873e2f6b4d8a706df5cfe034/full/original/0/default.jpg?X-Amz-Algorithm=AWS4-HMAC-SHA256&X-Amz-Date=20230613T134252Z&X-Amz-SignedHeaders=host&X-Amz-Expires=3600&X-Amz-Credential=AKIAINEAG7AQJT6DQBMQ%2F20230613%2Feu-west-1%2Fs3%2Faws4_request&X-Amz-Signature=712b4b98fa8939192458ff7bcc2cb9118aeb4b8e9ed68fcf6f2bbfb44d8c8bdc",
        
      }
    }
  ]
}

{#entities}Títol de l’exposició: {#activity}{title}{/activity}
{#objects}
{#object}
{display}
{#!portraitMedia}{!original}{/!portraitMedia}
{/object}
{/objects}
{/entities}

The expected output is 4 rows with objects info. Only the two first rows has portraitMedia.original but the output is showing the portraitMedia.original from root scope on the other two rows as well.

thanks for your time

Thanks for the reproduction, it is indeed a bug.

The parser was incorrect, it had a bug indeed !!

The parser should instead be :

			parser: (tag) => {
				const onlyDeepestScope = tag[0] === "!";
				if (onlyDeepestScope) {
					// Remove the leading "!", ie: "!name" => "name"
					tag = tag.substr(1);
				}
				return {
					get(scope, context) {
						if (onlyDeepestScope && context.num < context.scopePath.length) {
							return null;
						}
						return scope[tag];
					},
				};
			},

The difference is subtle, because the get() function was called multiple times. And since the tag was changed after the first get(). it would work only for the first item in the array, and after that, it would totally ignore the ! prefix.

Thanks for the issue, I will also be updating the documentation.

Online doc is now updated

I would be very grateful if you can endorse docxtemplater here (preferably by video) : https://testimonial.to/docxtemplater

This will increase the trust my users have in the library and project.

Hi, thanks for your help. It's working now.
The code is this thread is different from the documentation. There's an extra return. I dont know if it's correct as well.

Thanks, the code from the doc indeed had syntax errors.

Thanks for pointing that out.