Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow using {% (end)raw %} tags #1220

Merged
merged 2 commits into from
Jun 2, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 0 additions & 6 deletions docs/_markbind/variables.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,3 @@
<variable name="showBaseUrlCode">
<code>{<span></span>{ baseUrl }}</code>
</variable>

<variable name="showBaseUrlText">{<span></span>{ baseUrl }}</variable>

<variable name="markbind_blue">#00B0F0</variable>

<variable name="icon_arrow_down">:fas-arrow-down:</variable>
Expand Down
16 changes: 9 additions & 7 deletions docs/userGuide/markBindSyntaxOverview.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,13 +99,15 @@ As MarkBind uses [VueStrap](https://bootstrap-vue.js.org/docs/components/alert/)

[Nunjucks](https://mozilla.github.io/nunjucks/) is a JavaScript based templating tool. Here is a simple example:

<box><span>
`<ul>`<br>
<code>{<span></span>% for item in [1, 2, 3, 4] %<span></span>}</code><br>
&nbsp;&nbsp;`<li>`<code>Item {<span></span>{ item }}</code>`</li>`<br>
<code>{<span></span>% endfor %<span></span>}</code><br>
`</ul>`
</span></box>
{% raw %}
```html { .no-line-numbers }
<ul>
{% for item in [1, 2, 3, 4] %}
<li>Item {{ item }}</li>
{% endfor %}
</ul>
```
{% endraw %}

{{ icon_arrow_down }}

Expand Down
5 changes: 3 additions & 2 deletions docs/userGuide/syntax/icons.mbdf
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,9 @@ The advantage of font icons over emojis is font icons can be _styled_ to fit you
</include>

<box type="important">
The syntax for icons has changed, and the earlier {<span></span>{ prefix_name }} syntax has been deprecated. <br>
Please use the new :prefix-name: syntax instead.

The syntax for icons has changed, and the earlier {%raw%}`{{ prefix_name }}`{%endraw%} syntax has been deprecated. <br>
Please use the new `:prefix-name:` syntax instead.
</box>

###### Using Font Awesome Icons
Expand Down
10 changes: 6 additions & 4 deletions docs/userGuide/syntax/includes.mbdf
Original file line number Diff line number Diff line change
Expand Up @@ -130,10 +130,12 @@ In other words, **`<include>` interprets the reused code relative to the origina

In `article.md`:

<code>
# {<span></span>{ title }}<br>
Author: {<span></span>{ author }}
</code>
{% raw %}
```html
# {{ title }}<br>
Author: {{ author }}
```
{% endraw %}
</div>

These variables work the same way as variables in `_markbind/variables.md`, except that they only apply to the included file. They allow the included file to be reused as a template, for different source files using different variable values.
Expand Down
13 changes: 8 additions & 5 deletions docs/userGuide/syntax/links.mbdf
Original file line number Diff line number Diff line change
Expand Up @@ -32,16 +32,19 @@ Links to files of the generated site (e.g., an HTML page or an image file) can b

Absolute paths:
<div class="indented">
Links should start with {{ showBaseUrlCode }} (which represents the root directory of the project).

{{ icon_example }} Here's how to specify a link to (1) a page, and (2) an image, using the {{ showBaseUrlCode }}:
Links should start with {% raw %}`{{ baseUrl }}`{% endraw %} (which represents the root directory of the project).

1. <code>Click [here]({{ showBaseUrlCode }}/userGuide/reusingContents.html).</code>
2. `![](`{{ showBaseUrlCode }}`/images/preview.png)`
{{ icon_example }} Here's how to specify a link to (1) a page, and (2) an image, using the {% raw %}`{{ baseUrl }}`:

1. `Click [here]({{ baseUrl }}/userGuide/reusingContents.html).`
2. `![]({{ baseUrl }}/images/preview.png)`

<box type="important">
To ensure that links in the <code>_markbind/</code> folder work correctly across the entire site, they should be written as absolute paths, prepended with <code><span>{</span>{ baseUrl }}</code>.

To ensure that links in the <code>_markbind/</code> folder work correctly across the entire site, they should be written as absolute paths, prepended with `{{ baseUrl }}`.
</box>
{% endraw %}
</div>

Relative paths:
Expand Down
12 changes: 3 additions & 9 deletions docs/userGuide/syntax/pageLayouts.mbdf
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
<!---
TODO: Change to a proper escape tag once PR#1049 is ready.
-->
<variable name="mainContentBody">
<code>{<span></span>{ MAIN_CONTENT_BODY }}</code>
</variable>

## Page Layouts

**A _layout_ is a set of page-tweaks that can be applied to a page (or group of pages) in one go.**
Expand Down Expand Up @@ -110,7 +103,8 @@ afterSetup(() => {
In the `page.md` file of your layouts, it should come with the following reserved variable:

<box>
{{ mainContentBody }}

{%raw%}`{{ MAIN_CONTENT_BODY }}`{%endraw%}
</box>

which injects the actual page content in every page. This allows you to build layouts in different ways.
Expand All @@ -124,7 +118,7 @@ which injects the actual page content in every page. This allows you to build la
```

```html {heading="page.md"}
{{ mainContentBody }}
{%raw%}{{ MAIN_CONTENT_BODY }}{%endraw%}
<panel header="Statistics Formula for the class" type="primary">
<img src="path_to_your_formula.png" />
</panel>
Expand Down
23 changes: 21 additions & 2 deletions docs/userGuide/tipsAndTricks.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

<span id="escapingCharacters">

##### Tip: Escaping Characters
##### :fas-lightbulb: Escaping Characters

For Markdown syntax: To display a literal character that are normally used for Markdown formatting, add a backslash (`\`) in front of the character.

Expand All @@ -26,7 +26,26 @@ For Markdown syntax: To display a literal character that are normally used for M

</span>

##### Problem: Unwanted starting space in links and triggers
{% raw %}

##### :fas-lightbulb: Using {% raw %}{% endraw %} to display `{{ content }}`


To display the raw variable interpolation syntax using `{% raw %}{% endraw %}`, you would also need to add
the `v-pre` attribute using markdown-it-attrs or as a html attribute.

<box type="info">

This isn't necessary for `<code>` elements, markdown code fences and inline code though, which markbind automatically
adds `v-pre` for.

However, this does not change the need for `{% raw %}{% endraw %}`. Meaning, you can still use variables within your code!
</box>


{% endraw %}

##### :fas-info: Unwanted starting space in links and triggers

When you use links or triggers, you may encounter a situation where an unwanted space is being generated by MarkBind:

Expand Down
3 changes: 2 additions & 1 deletion docs/userGuide/usingPlugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,8 @@ removing such potential conflicts.
- Should return an array of string tag names to be blacklisted, with each tag name being at least 2 characters long.

<box type="important">
Note however, that variable interpolation syntax <code>{<span>{</span> variable_name <span>}</span>}</code> will act as per normal.

Note however, that variable interpolation syntax {% raw %}`{{ variable_name }}`{% endraw %} will act as per normal.
Meaning, the user would still be able to use variables in your special tags!
</box>

Expand Down
2 changes: 1 addition & 1 deletion src/lib/markbind/src/lib/markdown-it/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ markdownIt.use(require('markdown-it-mark'))
.use(require('markdown-it-table-of-contents'))
.use(require('markdown-it-task-lists'), {enabled: true})
.use(require('markdown-it-linkify-images'), {imgClass: 'img-fluid'})
.use(require('markdown-it-attrs'))
.use(require('./patches/markdown-it-attrs-nunjucks'))
.use(require('./markdown-it-dimmed'))
.use(require('./markdown-it-radio-button'))
.use(require('./markdown-it-block-embed'))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
/*
Patch for markdown-it-attrs and nunjuck's usage of {% ... %}
*/

const mdAttrsUtils = require('markdown-it-attrs/utils');

mdAttrsUtils.hasDelimiters = function (where, options) {

if (!where) {
throw new Error('Parameter `where` not passed. Should be "start", "middle", "end" or "only".');
}

/**
* @param {string} str
* @return {boolean}
*/
return function (str) {
// we need minimum three chars, for example {b}
let minCurlyLength = options.leftDelimiter.length + 1 + options.rightDelimiter.length;
if (!str || typeof str !== 'string' || str.length < minCurlyLength) {
return false;
}

function validCurlyLength (curly) {
let isClass = curly.charAt(options.leftDelimiter.length) === '.';
let isId = curly.charAt(options.leftDelimiter.length) === '#';
return (isClass || isId)
? curly.length >= (minCurlyLength + 1)
: curly.length >= minCurlyLength;
}

let start, end, slice, nextChar;
let rightDelimiterMinimumShift = minCurlyLength - options.rightDelimiter.length;
switch (where) {
case 'start':
// first char should be {, } found in char 2 or more
slice = str.slice(0, options.leftDelimiter.length);
start = slice === options.leftDelimiter ? 0 : -1;
end = start === -1 ? -1 : str.indexOf(options.rightDelimiter, rightDelimiterMinimumShift);
// check if next character is not one of the delimiters
nextChar = str.charAt(end + options.rightDelimiter.length);
if (nextChar && options.rightDelimiter.indexOf(nextChar) !== -1) {
end = -1;
}
break;

case 'end':
// last char should be }
start = str.lastIndexOf(options.leftDelimiter);
end = start === -1 ? -1 : str.indexOf(options.rightDelimiter, start + rightDelimiterMinimumShift);
end = end === str.length - options.rightDelimiter.length ? end : -1;
break;

case 'only':
// '{.a}'
slice = str.slice(0, options.leftDelimiter.length);
start = slice === options.leftDelimiter ? 0 : -1;
slice = str.slice(str.length - options.rightDelimiter.length);
end = slice === options.rightDelimiter ? str.length - options.rightDelimiter.length : -1;
break;
}

/*
Simple patch here - abort if the delimiters wrap around % %
*/
const isCharAfterStartPercent = str.charAt(start + 1) === '%';
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

*the only functional changes in this file are these 5 lines

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just to confirm, this won't break the other nunjucks templating syntax that use {% ...%} right?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yup, if anything it should 'fix' it!

e.g.

{% for x in y %}
{{ x }}
{% endfor %}

this previously works (and still does) because nunjucks rendering is the first step and long precedes markdown rendering, so the {% for/endfor %} is 'erased' long before markdown-it-attrs touches it.
It dosen't work in the case of {% raw/endraw %} because it is still present at the time of markdown rendering though

So this should give more flexibility in future restructuring of the rendering order (if ever needed)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That makes sense, thanks for clarifying!

const isCharBeforeEndPercent = str.charAt(end - 1) === '%';
if (isCharAfterStartPercent && isCharBeforeEndPercent) {
return false;
}

return start !== -1
&& end !== -1
&& validCurlyLength(str.substring(start, end + options.rightDelimiter.length));
};
};

module.exports = require('markdown-it-attrs');
3 changes: 3 additions & 0 deletions src/lib/markbind/src/parsers/componentParser.js
Original file line number Diff line number Diff line change
Expand Up @@ -432,6 +432,9 @@ function _parseThumbnailAttributes(node) {
function parseComponents(node) {
try {
switch (node.name) {
case 'code':
node.attribs['v-pre'] = '';
break;
case 'panel':
_parsePanelAttributes(node);
break;
Expand Down
51 changes: 49 additions & 2 deletions src/lib/markbind/src/utils/nunjuckUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,57 @@ const unescapedEnv = nunjucks.configure({ autoescape: false }).addFilter('date',

const START_ESCAPE_STR = '{% raw %}';
const END_ESCAPE_STR = '{% endraw %}';
const REGEX = new RegExp('{% *raw *%}(.*?){% *endraw *%}', 'gs');
const RAW_TAG_REGEX = new RegExp('{% *(end)?raw *%}', 'g');

/**
* Pads the outermost {% raw %} {% endraw %} pairs with {% raw %} {% endraw %} again.
* This allows variables and other nunjuck syntax inside {% raw %} {% endraw %} tags
* to be ignored by nunjucks until the final renderString call.
*/
function preEscapeRawTags(pageData) {
return pageData.replace(REGEX, `${START_ESCAPE_STR}$&${END_ESCAPE_STR}`);
// TODO simplify using re.matchAll once node v10 reaches 'eol'
// https://github.com/nodejs/Release#nodejs-release-working-group
const tagMatches = [];
let tagMatch = RAW_TAG_REGEX.exec(pageData);
while (tagMatch !== null) {
tagMatches.push(tagMatch);
tagMatch = RAW_TAG_REGEX.exec(pageData);
}

const tagInfos = Array.from(tagMatches, match => ({
isStartTag: !match[0].includes('endraw'),
index: match.index,
content: match[0],
}));

let numStartRawTags = 0; // nesting level of {% raw %}
let lastTokenEnd = 0;
const tokens = [];

for (let i = 0; i < tagInfos.length; i += 1) {
const { index, isStartTag, content } = tagInfos[i];
const currentTokenEnd = index + content.length;
tokens.push(pageData.slice(lastTokenEnd, currentTokenEnd));
lastTokenEnd = currentTokenEnd;

if (isStartTag) {
if (numStartRawTags === 0) {
// only pad outermost {% raw %} with an extra {% raw %}
tokens.push(START_ESCAPE_STR);
}
numStartRawTags += 1;
} else {
if (numStartRawTags === 1) {
// only pad outermost {% endraw %} with an extra {% endraw %}
tokens.push(END_ESCAPE_STR);
}
numStartRawTags -= 1;
}
}
// add the last token
tokens.push(pageData.slice(lastTokenEnd));

return tokens.join('');
}

module.exports = {
Expand Down
12 changes: 9 additions & 3 deletions test/functional/test_site/expected/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -317,8 +317,8 @@ <h4 id="focus-groups">Focus groups<a class="fa fa-anchor" href="#focus-groups" o
<div name="Referencing specified path in boilerplate">
<h1 id="path-within-the-boilerplate-folder-is-separately-specified">Path within the boilerplate folder is separately specified<a class="fa fa-anchor" href="#path-within-the-boilerplate-folder-is-separately-specified" onclick="event.stopPropagation()"></a></h1>
<p>Like static include, pages within the site should be able to use files located in folders within boilerplate.</p>
<p>Also, the boilerplate file name (e.g. <code>inside.md</code>) and the file that it is supposed to act as (<code>notInside.md</code>) can be different.</p>
<p>This file should behaves as if it is in the <code>requirements</code> folder:</p>
<p>Also, the boilerplate file name (e.g. <code v-pre="">inside.md</code>) and the file that it is supposed to act as (<code v-pre="">notInside.md</code>) can be different.</p>
<p>This file should behaves as if it is in the <code v-pre="">requirements</code> folder:</p>
<panel src="/test_site/requirements/NonFunctionalRequirements._include_.html"><template slot="_header">
<p>Tested with the folllowing include</p>
</template>
Expand Down Expand Up @@ -347,7 +347,7 @@ <h1 id="path-within-the-boilerplate-folder-is-separately-specified">Path within
<p><em>MarkBind supports .mbd files.</em></p>
</div>
<div>
<p><code>MarkBind supports .mbdf files.</code></p>
<p><code v-pre="">MarkBind supports .mbdf files.</code></p>
</div>
<p><strong>Include from another Markbind site</strong></p>
<div>
Expand Down Expand Up @@ -630,6 +630,12 @@ <h1 id="markbind-plugin-pre-render">Markbind Plugin Pre-render<a class="fa fa-an
</div>
<h2 class="no-index" id="level-2-header-inside-headingsearchindex-with-no-index-attribute-should-not-be-indexed">Level 2 header (inside headingSearchIndex) with no-index attribute should not be indexed<a class="fa fa-anchor" href="#level-2-header-inside-headingsearchindex-with-no-index-attribute-should-not-be-indexed" onclick="event.stopPropagation()"></a></h2>
<h6 class="always-index" id="level-6-header-outside-headingsearchindex-with-always-index-attribute-should-be-indexed">Level 6 header (outside headingSearchIndex) with always-index attribute should be indexed<a class="fa fa-anchor" href="#level-6-header-outside-headingsearchindex-with-always-index-attribute-should-be-indexed" onclick="event.stopPropagation()"></a></h6>
<p><strong>Test nunjucks raw tags</strong></p>
<p></p>
<div v-pre="">{{ variable interpolation syntax can be used with v-pre }}</div>
<div v-pre="">{{ nonExistentVariable }}</div>
<code v-pre="">{{ code elements should automatically be assigned v-pre }}</code>
<p></p>
<hr class="footnotes-sep">
<section class="footnotes">
<ol class="footnotes-list">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<p>User stories are brief (typically, 1-3 sentences) descriptions of what the system can do for the users,
written in the customers’ language. Often, user stories are written by the customers themselves.</p>
<p>A commonly used format for writing user stories is:<br>
<strong><code>As a</code></strong> <code>&lt;use type/role&gt;</code> <strong><code>I can</code></strong> <code>&lt;function&gt;</code> <strong><code>so that</code></strong> <code>&lt;benefit&gt;</code></p>
<strong><code v-pre="">As a</code></strong> <code v-pre="">&lt;use type/role&gt;</code> <strong><code v-pre="">I can</code></strong> <code v-pre="">&lt;function&gt;</code> <strong><code v-pre="">so that</code></strong> <code v-pre="">&lt;benefit&gt;</code></p>
<p>Here are some examples of user stories for the IVLE system:</p>
<pre><code class="hljs bat"><span>* As a student, I can download files uploaded by lecturers, so that I can get my own <span class="hljs-built_in">copy</span> of the files.<br></span><span>* As a lecturer, I can create discussion forums, so that students can discuss things online.<br></span><span>* As a tutor, I can <span class="hljs-built_in">print</span> attendance sheets, so that I can take attendance during the class.<br></span></code></pre>
<p>The <code>&lt;benefit&gt;</code> can be omitted if it is obvious. E.g. As a tutor, I can print attendance sheets.
<pre><code class="hljs bat" v-pre=""><span>* As a student, I can download files uploaded by lecturers, so that I can get my own <span class="hljs-built_in">copy</span> of the files.<br></span><span>* As a lecturer, I can create discussion forums, so that students can discuss things online.<br></span><span>* As a tutor, I can <span class="hljs-built_in">print</span> attendance sheets, so that I can take attendance during the class.<br></span></code></pre>
<p>The <code v-pre="">&lt;benefit&gt;</code> can be omitted if it is obvious. E.g. As a tutor, I can print attendance sheets.
User stories are mainly used for early estimation and scheduling purposes.</p>
<p>According to <ref xp-website="">this<ref></ref>, the biggest difference between user stories and traditional requirements
specifications is in the level of detail. User stories should only provide enough detail to make a reasonably low risk
Expand Down
Loading