Using nested shortcodes in safe mode

Introduction

Hugo has the option to embed shortcodes. Embedded shortcodes have access to their parent using the .Parent variable. The parent has no knowledge of its children, but can access its inner content (which includes any rendered children) using the .Inner variable.

For example, let’s consider the following shortcodes:

layouts/partials/inner.html

<p>{{ .Inner | .Page.RenderString }}</p>

layouts/partials/outer.html

<div>
    {{ .Inner }}
</div>

Calling these shortcodes from within a regular page would generate the following HTML output when Goldmark uses safe rendering (omitting HTML input from the output):

Markdown content

{{< outer >}}
    {{< inner >}}Plain content is fine{{< /inner >}}
    {{< inner >}}## Markdown content is fine{{< /inner >}}
    {{< inner >}}<span>HTML content</span> is rendered as plain text{{< /inner >}}
{{< /outer >}}

HTML output

<p>Plain content is fine</p>
<p><h2 id="markdown-content-is-fine">Markdown content is fine</h2>
</p>
<p><!-- raw HTML omitted -->HTML content<!-- raw HTML omitted --> is rendered as plain text</p>

Challenge

The approach of the outer shortcode is unsafe, as the call to {{ .Inner }} outputs any content to the final HTML page. For example, the following unsafe HTML is rendered to the output, despite Goldmark’s safe mode.

Unsafe Markdown content

{{< outer >}}
    {{< inner >}}Plain content is fine{{< /inner >}}
    {{< inner >}}## Markdown content is fine{{< /inner >}}
    {{< inner >}}<span>HTML content</span> is rendered as plain text{{< /inner >}}
    <mark>This unsafe code is not omitted</mark>
{{< /outer >}}

Unsafe HTML output

<p>Plain content is fine</p>
<p><h2 id="markdown-content-is-fine">Markdown content is fine</h2>
</p>
<p><!-- raw HTML omitted -->HTML content<!-- raw HTML omitted --> is rendered as plain text</p>
<mark>This unsafe code is not omitted</mark>

Suggested workaround

As the children are processed before the parent, and the parent has simply no knowledge of any children, we cannot use {{ .Inner | .Page.RenderString }} for outer.html. The variable .Inner contains generated HTML output of the various calls to inner.html. However, we cannot trust the .Inner content of outer.html as demonstrated earlier.

Instead, a possible workaround could be to be pass any generated output as scratch variable from inner.html to its parent (if any). The parent would consider that scratch variable as safe and generate that output. Any other (remaining) input can be ignored.

Implementation

This repository contains a working demo of the workaround. It is based on Hugo’s quick start.

Run the following commands to start a local development server.

git clone --recurse-submodules https://github.com/markdumay/shortcode-test.git
cd shortcode-test
hugo server

Navigate to your local site (usually localhost:1313) within your browser and click on My First Post. The post contains two paragraphs. The unsafe paragraph demonstrates the issue when using .Inner from within the parent. The safe paragraphs demonstrates the workaround that uses scratch variables instead. It also logs a warning to the console about unexpected input.

Closing thoughts

There might be a better and more elegant way how to handle embedded shortcodes. Suggestions are welcome!

1 Like