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

feat(options): Validate options passed to the parse function (#29) #44

Merged
Merged
Show file tree
Hide file tree
Changes from 5 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
84 changes: 48 additions & 36 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,51 +1,44 @@
const { loadFileStructureFromOptions } = require('./lib/helpers');
const { validate, retrieveFileOptions } = require('./lib/options');
const SvelteVersionDetector = require('./lib/detector');

const DEFAULT_ENCODING = 'utf8';
const DEFAULT_IGNORED_VISIBILITIES = ['protected', 'private'];
/**
* @typedef {import("./typings").SvelteParserOptions} SvelteParserOptions
*/

function validateOptions(options) {
if (!options || (!options.filename && !options.fileContent)) {
throw new Error('One of options.filename or options.filecontent is required');
}
}

function normalizeOptions(options) {
options.encoding = options.encoding || DEFAULT_ENCODING;
options.ignoredVisibilities = options.ignoredVisibilities || DEFAULT_IGNORED_VISIBILITIES;
}

function buildSvelte2Parser(structure, options) {
function buildSvelte2Parser(options) {
const Parser = require('./lib/parser');

// Convert structure object to old version source options
const hasScript = structure.scripts && structure.scripts.length > 0;
const hasStyle = structure.styles && structure.styles.length > 0;
const { scripts, styles, template } = options.structure;

const hasScript = !!scripts && scripts.length > 0;
const hasStyle = !!styles && styles.length > 0;

options.source = {
template: structure.template,
script: hasScript ? structure.scripts[0].content : '',
scriptOffset: hasScript ? structure.scripts[0].offset : 0,
style: hasStyle ? structure.styles[0].content : '',
styleOffset: hasStyle ? structure.styles[0].offset : 0,
template: template,
script: hasScript ? scripts[0].content : '',
scriptOffset: hasScript ? scripts[0].offset : 0,
style: hasStyle ? styles[0].content : '',
styleOffset: hasStyle ? styles[0].offset : 0,
};

return new Parser(options);
}

function buildSvelte3Parser(structure, options) {
function buildSvelte3Parser(options) {
const Parser = require('./lib/v3/parser');

return new Parser(structure, options);
return new Parser(options);
}

function buildSvelteParser(structure, options, version) {
function buildSvelteParser(options, version) {
if (version === SvelteVersionDetector.SVELTE_VERSION_3) {
return buildSvelte3Parser(structure, options);
return buildSvelte3Parser(options);
}

if (version === SvelteVersionDetector.SVELTE_VERSION_2) {
return buildSvelte2Parser(structure, options);
return buildSvelte2Parser(options);
}

if (version) {
Expand Down Expand Up @@ -116,9 +109,9 @@ function mergeItems(itemType, currentItem, newItem, ignoreLocations) {
return currentItem;
}

function subscribeOnParserEvents(parser, options, version, resolve, reject) {
function subscribeOnParserEvents(parser, ignoredVisibilities, detectedVersion, resolve, reject) {
alexprey marked this conversation as resolved.
Show resolved Hide resolved
const component = {
version: version
version: detectedVersion
};

parser.features.forEach((feature) => {
Expand Down Expand Up @@ -166,7 +159,7 @@ function subscribeOnParserEvents(parser, options, version, resolve, reject) {
parser.features.forEach((feature) => {
if (component[feature] instanceof Array) {
component[feature] = component[feature].filter((item) => {
return !options.ignoredVisibilities.includes(item.visibility);
return !ignoredVisibilities.includes(item.visibility);
});
}
});
Expand All @@ -179,27 +172,46 @@ function subscribeOnParserEvents(parser, options, version, resolve, reject) {
});
}

/**
* Main parse function.
* @param {SvelteParserOptions} options
* @example
* const { parse } = require('sveltedoc-parser');
* // basic usage only requires 'filename' to be set.
* const doc = await parse({
* filename: 'main.svelte',
* encoding: 'ascii',
* features: ['data', 'computed', 'methods'],
* ignoredVisibilities: ['private'],
* includeSourceLocations: true,
* version: 3
* });
*/
module.exports.parse = (options) => new Promise((resolve, reject) => {
try {
validateOptions(options);
normalizeOptions(options);
validate(options);

const fileOptions = retrieveFileOptions(options);

const structure = loadFileStructureFromOptions(options);
options.structure = loadFileStructureFromOptions(fileOptions);

const version = options.version || SvelteVersionDetector.detectVersionFromStructure(structure, options.defaultVersion);
const detectedVersion = options.version || SvelteVersionDetector.detectVersionFromStructure(structure, options.defaultVersion);

const parser = buildSvelteParser(structure, options, version);
const parser = buildSvelteParser(options, detectedVersion);

subscribeOnParserEvents(parser, options, version, resolve, reject);
subscribeOnParserEvents(parser, options.ignoredVisibilities, detectedVersion, resolve, reject);

parser.walk();
} catch (error) {
reject(error);
}
});

/**
* @param {SvelteParserOptions} options
*/
module.exports.detectVersion = (options) => {
validateOptions(options);
validate(options);

return SvelteVersionDetector.detectVersionFromOptions(options);
};
37 changes: 36 additions & 1 deletion lib/helpers.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
const fs = require('fs');
const path = require('path');

/**
* @typedef {import('../typings').SvelteParserOptions} SvelteParserOptions
* @typedef {'fileContent' | 'filename' | 'encoding'} FileOptionKeys
* @typedef {{content?: string; template?: string, scripts?: unknown[], styles?: unknown[] }} FileStructure
soft-decay marked this conversation as resolved.
Show resolved Hide resolved
* @typedef {Pick<SvelteParserOptions, FileOptionKeys> & { structure?: FileStructure }} FileOptions
*/

/**
* @typedef {{ offset: number, outerPosition: { start: number, end: number }, content: string, attributes: string }} HtmlBlock
*/

/**
*
* @param {FileOptions} options
*/
function loadFileStructureFromOptions(options) {
if (options.structure) {
return options.structure;
Expand Down Expand Up @@ -28,9 +43,17 @@ function loadFileStructureFromOptions(options) {
}
}

throw new Error('Can not load source from options, because nothing is not provided');
// With options validation, this should never happen
throw new Error('Internal Error: Cannot load FileStructure because input is missing.');
}

/**
*
* @param {string} content
* @param {string} blockName
* @param {number} searchStartIndex
* @returns {{ block: HtmlBlock | null }}
*/
function extractHtmlBlock(content, blockName, searchStartIndex) {
const blockOuterStartIndex = content.indexOf(`<${blockName}`, searchStartIndex);

Expand Down Expand Up @@ -65,7 +88,14 @@ function extractHtmlBlock(content, blockName, searchStartIndex) {
};
}

/**
*
* @param {string} content
* @param {string} blockName
* @returns {{ blocks: HtmlBlock[] }}
*/
function extractAllHtmlBlocks(content, blockName) {
/** @type {HtmlBlock[]} */
const blocks = [];

let searchResult = extractHtmlBlock(content, blockName);
Expand All @@ -83,6 +113,11 @@ function extractAllHtmlBlocks(content, blockName) {
};
}

/**
*
* @param {string} fileContent
* @returns {FileStructure}
*/
function parseFileStructureFromContent(fileContent) {
const scriptBlocksSearchResult = extractAllHtmlBlocks(fileContent, 'script');
const styleBlocksSearchResult = extractAllHtmlBlocks(fileContent, 'style');
Expand Down
151 changes: 151 additions & 0 deletions lib/options.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
const { isString, printArray, isVisibilitySupported, VISIBILITIES } = require('./utils');

/**
* @typedef {import("../typings").SvelteParserOptions} SvelteParserOptions
*/

/** @type {BufferEncoding[]} */
const ENCODINGS = [
'ascii',
'utf8',
'utf-8',
'utf16le',
'ucs2',
'ucs-2',
'base64',
'latin1',
'binary',
'hex'
];

const ERROR_ENCODING_FORMAT = 'Expected options.encoding to be a string. ';
const ERROR_VISIBILITIES_FORMAT = 'Expected options.ignoredVisibilities to be an array of strings. ';
const INFO_ENCODING_SUPPORTED = `Supported encodings: ${printArray(ENCODINGS)}.`;
const INFO_VISIBILITIES_SUPPORTED = `Supported visibilities: ${printArray(VISIBILITIES)}.`;

function getUnsupportedEncodingString(enc) {
return `encoding ${printArray([enc])} not supported. ` +
INFO_ENCODING_SUPPORTED;
}

function getUnsupportedVisibilitiesString(arr) {
return `Visibilities [${printArray(arr)}] in ` +
'options.ignoredVisibilities are not supported. ' +
INFO_VISIBILITIES_SUPPORTED;
}

const ErrorMessage = Object.freeze({
OptionsRequired: 'An options object is required.',
InputRequired: 'One of options.filename or options.fileContent is required.',
EncodingMissing: 'Internal Error: options.encoding is not set.',
EncodingFormat: ERROR_ENCODING_FORMAT + INFO_ENCODING_SUPPORTED,
EncodingNotSupported: (enc) => getUnsupportedEncodingString(enc),
IgnoredVisibilitiesMissing: 'Internal Error: options.ignoredVisibilities is not set.',
IgnoredVisibilitiesFormat: ERROR_VISIBILITIES_FORMAT + INFO_VISIBILITIES_SUPPORTED,
IgnoredVisibilitiesNotSupported: (arr) => `${getUnsupportedVisibilitiesString(arr)}`,
});

/** @type {BufferEncoding} */
const DEFAULT_ENCODING = 'utf8';

/** @type {SymbolVisibility[]} */
const DEFAULT_IGNORED_VISIBILITIES = ['protected', 'private'];

/** @returns {SvelteParserOptions} */
function getDefaultOptions() {
return {
encoding: DEFAULT_ENCODING,
ignoredVisibilities: [...DEFAULT_IGNORED_VISIBILITIES],
};
}

function retrieveFileOptions(options) {
return {
structure: options.structure,
fileContent: options.fileContent,
filename: options.filename,
encoding: options.encoding,
};
}

/**
* Applies default values to options.
* @param {SvelteParserOptions} options
*/
function normalize(options) {
const defaults = getDefaultOptions();

Object.keys(defaults).forEach((optionKey) => {
/**
* If the key was not set by the user, apply default value.
* This is better than checking for falsy values because it catches
* use cases were a user tried to do something not intended with
* an option (e.g. putting a value of 'false' or an empty string)
*/
if (!(optionKey in options)) {
options[optionKey] = defaults[optionKey];
}
});
}

/**
* @param {SvelteParserOptions} options
* @throws an error if any options are invalid
*/
function validate(options) {
if (!options) {
throw new Error(ErrorMessage.OptionsRequired);
}

normalize(options);

const hasFilename =
('filename' in options) &&
isString(options.filename) &&
options.filename.length > 0;

// Don't check length for fileContent because it could be an empty file.
const hasFileContent =
('fileContent' in options) &&
isString(options.fileContent);

if (!hasFilename && !hasFileContent) {
throw new Error(ErrorMessage.InputRequired);
}

if ('encoding' in options) {
if (!isString(options.encoding)) {
throw new Error(ErrorMessage.EncodingFormat);
}

if (!ENCODINGS.includes(options.encoding)) {
throw new Error(ErrorMessage.EncodingNotSupported(options.encoding));
}
} else {
// Sanity check. At this point, 'encoding' should be set.
throw new Error(ErrorMessage.EncodingMissing);
alexprey marked this conversation as resolved.
Show resolved Hide resolved
}

if ('ignoredVisibilities' in options) {
if (!Array.isArray(options.ignoredVisibilities)) {
throw new Error(ErrorMessage.IgnoredVisibilitiesFormat);
}

if (!options.ignoredVisibilities.every(isVisibilitySupported)) {
const notSupported = options.ignoredVisibilities.filter(
(iv) => !isVisibilitySupported(iv)
);

throw new Error(ErrorMessage.IgnoredVisibilitiesNotSupported(notSupported));
}
} else {
// Sanity check. At this point, 'ignoredVisibilities' should be set.
throw new Error(ErrorMessage.IgnoredVisibilitiesMissing);
}
}

module.exports = {
ErrorMessage: ErrorMessage,
validate: validate,
retrieveFileOptions: retrieveFileOptions,
};
3 changes: 2 additions & 1 deletion lib/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ const path = require('path');
const utils = require('./utils');
const jsdoc = require('./jsdoc');

const hasOwnProperty = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);
// TODO: Remove this and use from utils directly
soft-decay marked this conversation as resolved.
Show resolved Hide resolved
const hasOwnProperty = utils.hasOwnProperty;

const DEFAULT_OPTIONS = {
/**
Expand Down
Loading