Skip to content
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: 5 additions & 1 deletion docs/devGuide/development/writingPlugins.md
Original file line number Diff line number Diff line change
Expand Up @@ -160,6 +160,9 @@ module.exports = {
},
],
},
'my-custom-element': {
isCustomElement: true,
},
},
}
```
Expand All @@ -170,7 +173,8 @@ Tag properties are top-level properties of the tag configuration object. The fol

Property | Values | Default | Remarks
:----- | ------- | ---- | ----
`isSpecial` | `true` or `false` | `false` | Allows configuring whether any tag is to be parsed "specially" like a `<script>` or `<style>` tag. This allows configuring custom tags that may contain conflicting syntax, such as the `<puml>` tag used for UML diagram generation.
`isSpecial` | `true` or `false` | `false` | When `true`, the content inside the tag is treated as raw text and will **not** be parsed as HTML or Markdown (similar to `<script>` or `<style>` tags). Use this for tags containing non-HTML syntax that should not be processed (e.g. `<puml>` for diagrams). Use `false` for standard components that wrap renderable content.
`isCustomElement` | `true` or `false` | `false` | Tells the Vue compiler to ignore this tag during compilation, to prevent warnings about unknown custom elements. Useful for plugins that introduce custom HTML elements (e.g. Web Components) that should not be resolved as Vue components.
`attributes` | Array of attribute configurations | `[]` | Contains the attribute configurations of the tags.

**Attribute Properties**
Expand Down
21 changes: 21 additions & 0 deletions packages/core/src/Page/PageVueServerRenderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,28 @@ import fs from 'fs-extra';
import * as logger from '../utils/logger';
import type { PageConfig, PageAssets } from './PageConfig';
import type { Page } from '.';
import { PluginManager } from '../plugins/PluginManager';

/* eslint-enable import/no-import-module-exports */

let bundle = require('@markbind/core-web/dist/js/vueCommonAppFactory.min');

let customElementTagsCache: Set<string> | undefined;

/**
* Retrieves the set of tags that should be treated as custom elements by the Vue compiler.
* These are tags defined in plugins with isCustomElement: true.
*/
function getCustomElementTags(): Set<string> {
if (customElementTagsCache) {
return customElementTagsCache;
}
customElementTagsCache = new Set(Object.entries(PluginManager.tagConfig)
.filter(([, config]) => config.isCustomElement)
.map(([tagName]) => tagName));
return customElementTagsCache;
}

/**
* Compiles a Vue page template into a JavaScript function returning render function
* and saves it as a script file so that the browser can access to
Expand All @@ -32,11 +50,14 @@ let bundle = require('@markbind/core-web/dist/js/vueCommonAppFactory.min');
*/
async function compileVuePageCreateAndReturnScript(
content: string, pageConfig: PageConfig, pageAsset: PageAssets) {
const customElementTags = getCustomElementTags();

const compilerOptions: CompilerOptions = {
runtimeModuleName: 'vue',
runtimeGlobalName: 'Vue',
mode: 'function',
whitespace: 'preserve',
isCustomElement: tag => customElementTags.has(tag),
};

const templateOptions: SFCTemplateCompileOptions = {
Expand Down
17 changes: 10 additions & 7 deletions packages/core/src/html/linkProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,13 +91,16 @@ export function convertRelativeLinks(node: MbNode, cwf: string, rootPath: string
_convertRelativeLink(node, cwf, rootPath, baseUrl, resourcePath, linkAttribName);
}

if (node.name in pluginTagConfig && pluginTagConfig[node.name].attributes) {
pluginTagConfig[node.name].attributes.forEach((attrConfig) => {
if (attrConfig.isRelative && node.attribs) {
const resourcePath = node.attribs[attrConfig.name];
_convertRelativeLink(node, cwf, rootPath, baseUrl, resourcePath, attrConfig.name);
}
});
if (node.name in pluginTagConfig) {
const tagConfig = pluginTagConfig[node.name];
if (tagConfig.attributes) {
tagConfig.attributes.forEach((attrConfig) => {
if (attrConfig.isRelative && node.attribs) {
const resourcePath = node.attribs[attrConfig.name];
_convertRelativeLink(node, cwf, rootPath, baseUrl, resourcePath, attrConfig.name);
}
});
}
}
}

Expand Down
5 changes: 3 additions & 2 deletions packages/core/src/plugins/Plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,9 @@ type TagConfigAttributes = {
};

export type TagConfigs = {
isSpecial: boolean,
attributes: TagConfigAttributes[]
isSpecial?: boolean,
attributes?: TagConfigAttributes[],
isCustomElement?: boolean,
};

/**
Expand Down
91 changes: 91 additions & 0 deletions packages/core/test/unit/Page/PageVueServerRenderer.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
jest.mock('../../../src/plugins/PluginManager');
jest.mock('vue/compiler-sfc');
jest.mock('fs-extra');

describe('PageVueServerRenderer', () => {
let pageVueServerRenderer: any;
let PluginManager: any;
let compileTemplate: any;

beforeEach(async () => {
jest.resetModules();

// Re-acquire dependencies for the new module context (As Tags are cached in PluginManager)
PluginManager = (await import('../../../src/plugins/PluginManager')).PluginManager;
compileTemplate = (await import('vue/compiler-sfc')).compileTemplate;
pageVueServerRenderer = (await import('../../../src/Page/PageVueServerRenderer')).pageVueServerRenderer;
});

afterEach(() => {
jest.clearAllMocks();
});

describe('compileVuePageCreateAndReturnScript', () => {
test('correctly filters isCustomElement flag elements, '
+ 'passes to Vue compiler isCustomElement option', async () => {
// Setup
(PluginManager as any).tagConfig = {
'my-custom-element': {
isCustomElement: true,
},
'regular-element': {
isCustomElement: false,
},
};

(compileTemplate as jest.Mock).mockReturnValue({
code: '/* compiled code */',
});

const pageConfig = {
sourcePath: 'test.md',
resultPath: 'test.html',
};
const pageAsset = {};

// Execute
await pageVueServerRenderer.compileVuePageCreateAndReturnScript(
'content',
pageConfig as any,
pageAsset as any,
);

// Verify
expect(compileTemplate).toHaveBeenCalled();

const { compilerOptions } = (compileTemplate as jest.Mock).mock.calls[0][0];
const { isCustomElement } = compilerOptions;

expect(isCustomElement('my-custom-element')).toBe(true);
expect(isCustomElement('regular-element')).toBe(false);
expect(isCustomElement('div')).toBe(false);
});

test('handles empty tagConfig or tags without isCustomElement property', async () => {
(PluginManager as any).tagConfig = {
'some-tag': {
// isSpecial and attributes are optional now
},
};

(compileTemplate as jest.Mock).mockReturnValue({
code: '/* compiled code */',
});

const pageConfig = { sourcePath: 'test.md', resultPath: 'test.html' };
const pageAsset = {};

await pageVueServerRenderer.compileVuePageCreateAndReturnScript(
'content',
pageConfig as any,
pageAsset as any,
);

const { compilerOptions } = (compileTemplate as jest.Mock).mock.calls[0][0];
const { isCustomElement } = compilerOptions;

expect(isCustomElement('some-tag')).toBe(false);
expect(isCustomElement('any-tag')).toBe(false);
});
});
});
Loading