const path = require('path');
const { provider } = require('@homer0/jimple');
/**
* A basic class to handle single file components' (SFC) data and rendering for the app.
*/
class SFCData {
/**
* A shorthand to create a new instance of the class.
*
* @param {string} filepath The file path of the component for which the data will be
* saved. This is later used when merging components in order
* to fix relative paths between files.
* @returns {SFCData}
* @static
*/
static new(filepath) {
return new SFCData(filepath);
}
/**
* @param {string} filepath The file path of the component for which the data will be
* saved. This is later used when merging components in order
* to fix relative paths between files.
*/
constructor(filepath) {
/**
* The path of the SFC.
*
* @type {string}
* @access protected
* @ignore
*/
this._filepath = filepath;
/**
* The directory where the SFC is located.
*
* @type {string}
* @access protected
* @ignore
*/
this._directory = path.dirname(this._filepath);
/**
* The HTML markup of the SFC; this doesn't include scripts and styles.
*
* @type {string}
* @access protected
* @ignore
*/
this._markup = '';
/**
* The list of script tags the SFC has.
*
* @type {SFCTag[]}
* @access protected
* @ignore
*/
this._scripts = [];
/**
* The list of module script tags (those with the `context="module"` attribute) the
* SFC has.
*
* @type {SFCTag[]}
* @access protected
* @ignore
*/
this._moduleScripts = [];
/**
* The list of style tags the SFC has.
*
* @type {SFCTag[]}
* @access protected
* @ignore
*/
this._styles = [];
/**
* In case the SFC extends another SFC, this will be a reference for it.
*
* @type {?SFCData}
* @access protected
* @ignore
*/
this._baseFile = null;
/**
* In case the SFC extends another SFC, this dictionary will contain the information
* of the `<extend />` tag.
*
* @type {Object}
* @access protected
* @ignore
*/
this._extendTagAttributes = {};
}
/**
* Adds the information of a SFC this one is extending.
*
* @param {SFCData} fileData The SFC information.
* @param {Object} extendTagAttributes The attributes of this SFC `<extend />` tag.
* @throws {Error} If this SFC already has a base SFC already set.
* @throws {Error} If the `fileData` is not an instance of {@link SFCData}.
*/
addBaseFileData(fileData, extendTagAttributes = {}) {
if (this._baseFile) {
throw new Error("You can't add more than one base file data");
} else if (!(fileData instanceof SFCData)) {
throw new Error('`fileData` must be an instance of SFCData');
}
this._baseFile = fileData;
this._extendTagAttributes = extendTagAttributes;
}
/**
* Adds HTML markup for the SFC. If there's already markup saved, it will just append
* it.
*
* @param {string} content The HTML code to add.
*/
addMarkup(content) {
const newMarkup = this._markup ? `${this._markup}\n${content}` : content;
this._markup = newMarkup;
}
/**
* Adds a script tag information to the SFC.
*
* @param {string} content The contents of the tag.
* @param {Object} attributes A dictionary with the tag attributes.
*/
addScript(content, attributes = {}) {
const list = attributes.context === 'module' ? this._moduleScripts : this._scripts;
list.push({
content,
attributes,
});
}
/**
* Adds a style tag information to the SFC.
*
* @param {string} content The contents of the tag.
* @param {Object} attributes A dictionary with the tag attributes.
*/
addStyle(content, attributes = {}) {
this._styles.push({
content,
attributes,
});
}
/**
* Renders the whole SFC information into a string, so it can be saved on a file.
*
* @returns {string}
*/
render() {
const lines = [];
if (this.hasModuleScripts) {
lines.push(this._renderTag('script', this.moduleScript));
}
if (this.hasScripts) {
lines.push(this._renderTag('script', this.script));
}
if (this.hasStyles) {
lines.push(this._renderTag('style', this.style));
}
lines.push(this.markup);
return lines.join('\n');
}
/**
* In case the SFC extends another SFC, this will be a reference for it.
*
* @type {?SFCData}
*/
get baseFileData() {
return this._baseFile;
}
/**
* The directory where the SFC is located.
*
* @type {string}
*/
get directory() {
return this._directory;
}
/**
* In case the SFC extends another SFC, this dictionary will contain the information of
* the `<extend />` tag.
*
* @type {Object}
*/
get extendTagAttributes() {
return this._extendTagAttributes;
}
/**
* The path of the SFC.
*
* @type {string}
*/
get filepath() {
return this._filepath;
}
/**
* Whether or not the SFC extends another SFC.
*
* @type {boolean}
*/
get hasBaseFileData() {
return this._baseFile !== null;
}
/**
* Whether or not the SFC has module script tags (those with the `context="module"`
* attribute).
*
* @type {boolean}
*/
get hasModuleScripts() {
return this._moduleScripts.length > 0;
}
/**
* Whether or not the SFC has script tags.
*
* @type {boolean}
*/
get hasScripts() {
return this._scripts.length > 0;
}
/**
* Whether or not the SFC has style tags.
*
* @type {boolean}
*/
get hasStyles() {
return this._styles.length > 0;
}
/**
* The HTML markup of the SFC; this doesn't include scripts and styles.
*
* @type {string}
*/
get markup() {
return this._markup;
}
/**
* A single {@link SFCTag} that merges the contents and attributes of all the module
* scripts tags (those with the `context="module"` attribute) the SFC has.
*
* @type {SFCTag}
*/
get moduleScript() {
const result = this._mergeTags(this._moduleScripts);
result.attributes.context = 'module';
return result;
}
/**
* The list of module script tags (those with the `context="module"` attribute) the SFC
* has.
*
* @type {SFCTag[]}
*/
get moduleScripts() {
return this._moduleScripts;
}
/**
* A single {@link SFCTag} that merges the contents and attributes of all the script
* tags the SFC has.
*
* @type {SFCTag}
*/
get script() {
return this._mergeTags(this._scripts);
}
/**
* The list of script tags the SFC has.
*
* @type {SFCTag[]}
*/
get scripts() {
return this._scripts;
}
/**
* A single {@link SFCTag} that merges the contents and attributes of all the style tags
* the SFC has.
*
* @type {SFCTag}
*/
get style() {
return this._mergeTags(this._styles);
}
/**
* The list of style tags the SFC has.
*
* @type {SFCTag[]}
*/
get styles() {
return this._styles;
}
/**
* A utility method that merges a list of tags into a single one.
*
* @param {SFCTag[]} tags The list of tags to merge.
* @returns {SFCTag}
* @access protected
* @ignore
*/
_mergeTags(tags) {
let result;
if (tags.length === 0) {
result = {
content: '',
attributes: {},
};
} else if (tags.length === 1) {
[result] = tags;
} else {
result = tags.reduce(
(acc, tag) => ({
content: `${acc.content}\n${tag.content}`,
attributes: { ...acc.attributes, ...tag.attributes },
}),
{
content: '',
attributes: {},
},
);
result.content = result.content.replace(/^\n/, '');
}
return result;
}
/**
* Renders a {@link SFCTag} on a string.
*
* @param {string} name The name of the tag (like `script` or `style`).
* @param {SFCTag} tag The tag information.
* @returns {string}
* @access protected
* @ignore
*/
_renderTag(name, tag) {
const attrsNames = Object.keys(tag.attributes);
let attrs;
if (attrsNames.length) {
attrs = attrsNames
.reduce((acc, attrName) => {
const value = tag.attributes[attrName];
return [...acc, `${attrName}="${value}"`];
}, [])
.join(' ');
attrs = ` ${attrs}`;
} else {
attrs = '';
}
return [`<${name}${attrs}>`, tag.content, `</${name}>`].join('\n');
}
}
/**
* The service provider that once registered on {@link SvelteExtend} will save the class
* {@link SFCData} as the `sfcData` service.
*
* @type {Provider}
*/
const sfcData = provider((app) => {
app.set('sfcData', () => SFCData);
});
module.exports = {
SFCData,
sfcData,
};