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

Support context-aware includes for images and anchors #826

Merged
merged 5 commits into from
Apr 18, 2019
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
2 changes: 1 addition & 1 deletion docs/userGuide/plugins/filterTags.mbdf
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ Alternatively, you can specify tags to render for a page in the front matter.
```
</span>

Tags in `site.json` will be merged with the ones in the front matter, and are processed after front matter tags. See [Hiding Tags](userGuide/tweakingThePageStructure.html#hiding-tags) for more information.
Tags in `site.json` will be merged with the ones in the front matter, and are processed after front matter tags. See [Hiding Tags](../tweakingThePageStructure.html#hiding-tags) for more information.

#### Advanced Tagging Tips

Expand Down
4 changes: 4 additions & 0 deletions docs/userGuide/syntax/images.mbdf
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
```markdown
![](https://markbind.org/images/logo-lightbackground.png)
```
<box type="info">
URLs can be specified as relative references. More info in: <i><a href="#intraSiteLinks">Intra-Site Links</a></i>
</box>

</span>
<span id="output">

Expand Down
10 changes: 7 additions & 3 deletions docs/userGuide/syntax/links.mbdf
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,14 @@ Links should start with {{ showBaseUrlCode }} (which represents the root directo
1. <code>Click [here]({{ showBaseUrlCode }}/userGuide/reusingContents.html).</code>
2. `![](`{{ showBaseUrlCode }}`/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>.
</box>
</div>

Relative paths:

<div class="indented">
Links to files can also be specified relative to the file that includes it.

{{ icon_example }} Assuming that we have the following folder structure:
```
Expand All @@ -83,12 +85,14 @@ Within `index.md`, we can also display the image using
<img src="textbook/image.png" />
```
or by including `subsite.md`:
```
```html
<include src="textbook/subsite.md" />
```

<box type="important">
To ensure that links in the _markbind/ folder work correctly across the entire site, they should be written as absolute paths, prepended with <span>{</span>{ baseUrl }}.
Relative links to resources (e.g. images, hrefs) should be valid relative to the original, included file. In other words, the links should be accessible when traversing starting from the location of the included file.
<br>
In the example above, <code>image.png</code> is in the same directory as <code>subsite.md</code>. When using relative references, the correct path is <code>image.png</code> and not <code>textbook/image.png</code>.
sijie123 marked this conversation as resolved.
Show resolved Hide resolved
</box>

</div>
Expand Down
2 changes: 1 addition & 1 deletion docs/userGuide/syntax/pictures.mbdf
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Name | Type | Default | Description
--- | --- | --- | ---
alt | `string` | | **This must be specified.**<br>The alternative text of the image.
height | `string` | | The height of the image in pixels.
src | `string` | | **This must be specified.**<br>The URL of the image.
src | `string` | | **This must be specified.**<br>The URL of the image.<br>The URL can be specified as absolute or relative references. More info in: _[Intra-Site Links]({{baseUrl}}/userGuide/formattingContents.html#intraSiteLinks)_
width | `string` | | The width of the image in pixels.<br>If both width and height are specified, width takes priority over height. It is to maintain the image's aspect ratio.

<span id="short" class="d-none">
Expand Down
3 changes: 3 additions & 0 deletions src/Page.js
Original file line number Diff line number Diff line change
Expand Up @@ -804,6 +804,8 @@ Page.prototype.generate = function (builtFiles) {
.then(() => markbinder.renderFile(this.tempPath, fileConfig))
.then(result => this.postRender(result))
.then(result => this.collectPluginsAssets(result))
.then(result => markbinder.processDynamicResources(this.sourcePath, result))
.then(result => markbinder.unwrapIncludeSrc(result))
.then((result) => {
this.content = htmlBeautify(result, { indent_size: 2 });

Expand Down Expand Up @@ -980,6 +982,7 @@ Page.prototype.resolveDependency = function (dependency, builtFiles) {
baseUrlMap: this.baseUrlMap,
rootPath: this.rootPath,
}))
.then(result => markbinder.processDynamicResources(file, result))
.then((result) => {
// resolve the site base url here
const newBaseUrl = calculateNewBaseUrl(file, this.rootPath, this.baseUrlMap);
Expand Down
107 changes: 76 additions & 31 deletions src/lib/markbind/src/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -172,24 +172,20 @@ Parser.prototype._preprocess = function (node, context, config) {
element.attribs = element.attribs || {};
element.attribs[ATTRIB_CWF] = path.resolve(context.cwf);

const requiresSrc = ['img', 'pic', 'include'].includes(element.name);
const requiresSrc = ['include'].includes(element.name);
if (requiresSrc && _.isEmpty(element.attribs.src)) {
const error = new Error(`Empty src attribute in ${element.name} in: ${element.attribs[ATTRIB_CWF]}`);
this._onError(error);
return createErrorNode(element, error);
}
const shouldProcessSrc = ['img', 'pic', 'include', 'panel'].includes(element.name);
const shouldProcessSrc = ['include', 'panel'].includes(element.name);
const hasSrc = _.hasIn(element.attribs, 'src');
let isUrl;
let includeSrc;
let filePath;
let isAbsolutePath;
let actualFilePath;
if (hasSrc && shouldProcessSrc) {
isUrl = utils.isUrl(element.attribs.src);
isAbsolutePath = path.isAbsolute(element.attribs.src)
|| element.attribs.src.includes('{{baseUrl}}')
|| element.attribs.src.includes('{{hostBaseUrl}}');
includeSrc = url.parse(element.attribs.src);
filePath = isUrl
? element.attribs.src
Expand All @@ -202,7 +198,7 @@ Parser.prototype._preprocess = function (node, context, config) {
this.boilerplateIncludeSrc.push({ from: context.cwf, to: actualFilePath });
}
const isOptional = element.name === 'include' && _.hasIn(element.attribs, 'optional');
if (!['img', 'pic'].includes(element.name) && !utils.fileExists(actualFilePath)) {
if (!utils.fileExists(actualFilePath)) {
if (isOptional) {
return createEmptyNode();
}
Expand All @@ -215,18 +211,6 @@ Parser.prototype._preprocess = function (node, context, config) {
}
}

const shouldProcessHref = ['a', 'link'].includes(element.name);
const hasHref = _.hasIn(element.attribs, 'href');
if (hasHref && shouldProcessHref) {
isUrl = utils.isUrl(element.attribs.href);
isAbsolutePath = path.isAbsolute(element.attribs.href) || element.attribs.href.startsWith('{{');
includeSrc = url.parse(element.attribs.href);
filePath = isUrl
? element.attribs.src
: path.resolve(path.dirname(context.cwf), decodeURIComponent(includeSrc.path));
actualFilePath = filePath;
}

if (element.name === 'include') {
const isInline = _.hasIn(element.attribs, 'inline');
const isDynamic = _.hasIn(element.attribs, 'dynamic');
Expand Down Expand Up @@ -341,7 +325,11 @@ Parser.prototype._preprocess = function (node, context, config) {
}
actualContent = self._rebaseReferenceForStaticIncludes(actualContent, element, config);
}
element.children = cheerio.parseHTML(actualContent, true); // the needed content;
const wrapperType = isInline ? 'span' : 'div';
element.children = cheerio.parseHTML(
`<${wrapperType} data-included-from="${filePath}">${actualContent}</${wrapperType}>`,
true,
);
} else {
let actualContent = (fileContent && isTrim) ? fileContent.trim() : fileContent;
if (isIncludeSrcMd) {
Expand All @@ -351,7 +339,11 @@ Parser.prototype._preprocess = function (node, context, config) {
actualContent = md.render(actualContent);
}
}
element.children = cheerio.parseHTML(actualContent, true);
const wrapperType = isInline ? 'span' : 'div';
element.children = cheerio.parseHTML(
`<${wrapperType} data-included-from="${filePath}">${actualContent}</${wrapperType}>`,
true,
);
}

// The element's children are in the new context
Expand Down Expand Up @@ -383,16 +375,6 @@ Parser.prototype._preprocess = function (node, context, config) {
if (element.name === 'body') {
// eslint-disable-next-line no-console
console.warn(`<body> tag found in ${element.attribs[ATTRIB_CWF]}. This may cause formatting errors.`);
} else if (['img', 'pic'].includes(element.name)) {
if (!isUrl && !isAbsolutePath) {
const resultPath = path.join('{{hostBaseUrl}}', path.relative(config.rootPath, filePath));
element.attribs.src = utils.ensurePosix(resultPath);
}
} else if (['a', 'link'].includes(element.name)) {
if (!isUrl && !isAbsolutePath && hasHref) {
const resultPath = path.join('{{hostBaseUrl}}', path.relative(config.rootPath, filePath));
element.attribs.href = utils.ensurePosix(resultPath);
}
}
if (element.children && element.children.length > 0) {
element.children = element.children.map(e => self._preprocess(e, context, config));
Expand All @@ -402,6 +384,69 @@ Parser.prototype._preprocess = function (node, context, config) {
return element;
};

Parser.prototype.processDynamicResources = function (context, html) {
const self = this;
const $ = cheerio.load(html, {
xmlMode: false,
decodeEntities: false,
});
$('img, pic').each(function () {
const elem = $(this);
const resourcePath = utils.ensurePosix(elem.attr('src'));
if (resourcePath === undefined || resourcePath === '') {
// Found empty img/pic resource in resourcePath
return;
}
if (utils.isAbsolutePath(resourcePath) || utils.isUrl(resourcePath)) {
// Do not rewrite.
return;
}
const firstParent = elem.closest('div[data-included-from], span[data-included-from]');
const originalSrc = utils.ensurePosix(firstParent.attr('data-included-from') || context);

const originalSrcFolder = path.posix.dirname(originalSrc);
const fullResourcePath = path.posix.join(originalSrcFolder, resourcePath);
const resolvedResourcePath = path.posix.relative(utils.ensurePosix(self.rootPath), fullResourcePath);
const absoluteResourcePath = path.posix.join('{{hostBaseUrl}}', resolvedResourcePath);

$(this).attr('src', absoluteResourcePath);
});
$('a, link').each(function () {
const elem = $(this);
const resourcePath = elem.attr('href');
if (resourcePath === undefined || resourcePath === '') {
// Found empty href resource in resourcePath
return;
}
if (utils.isAbsolutePath(resourcePath) || utils.isUrl(resourcePath) || resourcePath.startsWith('#')) {
// Do not rewrite.
return;
}

const firstParent = elem.closest('div[data-included-from], span[data-included-from]');
const originalSrc = utils.ensurePosix(firstParent.attr('data-included-from') || context);

const originalSrcFolder = path.posix.dirname(originalSrc);
const fullResourcePath = path.posix.join(originalSrcFolder, resourcePath);
const resolvedResourcePath = path.posix.relative(utils.ensurePosix(self.rootPath), fullResourcePath);
const absoluteResourcePath = path.posix.join('{{hostBaseUrl}}', resolvedResourcePath);

$(this).attr('href', absoluteResourcePath);
});
return $.html();
};

Parser.prototype.unwrapIncludeSrc = function (html) {
const $ = cheerio.load(html, {
xmlMode: false,
decodeEntities: false,
});
$('div[data-included-from], span[data-included-from]').each(function () {
$(this).replaceWith($(this).contents());
});
return $.html();
};

Parser.prototype._parse = function (node, context, config) {
const element = node;
const self = this;
Expand Down
6 changes: 6 additions & 0 deletions src/lib/markbind/src/utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,12 @@ module.exports = {
return r.test(filePath);
},

isAbsolutePath(filePath) {
return path.isAbsolute(filePath)
|| filePath.includes('{{baseUrl}}')
|| filePath.includes('{{hostBaseUrl}}');
},

createErrorElement(error) {
return `<div style="color: red">${error.message}</div>`;
},
Expand Down
33 changes: 20 additions & 13 deletions test/unit/parser.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,10 +45,11 @@ test('includeFile replaces <include> with <div>', async () => {

const expected = [
'# Index',
`<div cwf="${indexPath}" include-path="${includePath}">`,
`<div cwf="${indexPath}" include-path="${includePath}">`
+ `<div data-included-from="${includePath}" cwf="${includePath}">`,
yamgent marked this conversation as resolved.
Show resolved Hide resolved
'',
'# Include',
'</div>',
'</div></div>',
'',
].join('\n');

Expand Down Expand Up @@ -85,10 +86,11 @@ test('includeFile replaces <include src="exist.md" optional> with <div>', async

const expected = [
'# Index',
`<div cwf="${indexPath}" include-path="${existPath}">`,
`<div cwf="${indexPath}" include-path="${existPath}">`
+ `<div data-included-from="${existPath}" cwf="${existPath}">`,
'',
'# Exist',
'</div>',
'</div></div>',
'',
].join('\n');

Expand Down Expand Up @@ -161,10 +163,11 @@ test('includeFile replaces <include src="include.md#exists"> with <div>', async

const expected = [
'# Index',
`<div cwf="${indexPath}" include-path="${includePath}">`,
`<div cwf="${indexPath}" include-path="${includePath}">`
+ `<div data-included-from="${includePath}" cwf="${includePath}">`,
'',
'existing segment',
'</div>',
'</div></div>',
'',
].join('\n');

Expand Down Expand Up @@ -205,7 +208,8 @@ test('includeFile replaces <include src="include.md#exists" inline> with inline

const expected = [
'# Index',
`<span cwf="${indexPath}" include-path="${includePath}">existing segment</span>`,
`<span cwf="${indexPath}" include-path="${includePath}">`
+ `<span data-included-from="${includePath}" cwf="${includePath}">existing segment</span></span>`,
'',
].join('\n');

Expand Down Expand Up @@ -245,10 +249,11 @@ test('includeFile replaces <include src="include.md#exists" trim> with trimmed c

const expected = [
'# Index',
`<div cwf="${indexPath}" include-path="${includePath}">`,
`<div cwf="${indexPath}" include-path="${includePath}">`
+ `<div data-included-from="${includePath}" cwf="${includePath}">`,
'',
'existing segment',
'</div>',
'</div></div>',
'',
].join('\n');

Expand Down Expand Up @@ -332,10 +337,11 @@ test('includeFile replaces <include src="include.md#exists" optional> with <div>

const expected = [
'# Index',
`<div cwf="${indexPath}" include-path="${includePath}">`,
`<div cwf="${indexPath}" include-path="${includePath}">`
+ `<div data-included-from="${includePath}" cwf="${includePath}">`,
'',
'existing segment',
'</div>',
'</div></div>',
'',
].join('\n');

Expand Down Expand Up @@ -372,10 +378,11 @@ test('includeFile replaces <include src="include.md#doesNotExist" optional> with

const expected = [
'# Index',
`<div cwf="${indexPath}" include-path="${includePath}">`,
`<div cwf="${indexPath}" include-path="${includePath}">`
+ `<div data-included-from="${includePath}" cwf="${includePath}">`,
'',
'',
'</div>',
'</div></div>',
'',
].join('\n');

Expand Down