Skip to content

Commit

Permalink
Merge pull request #44 from soft-decay/feature/parser-options-validation
Browse files Browse the repository at this point in the history
feat(options): Validate options passed to the parse function (#29)
  • Loading branch information
alexprey authored Dec 3, 2020
2 parents acca60d + a438a40 commit 3bcb993
Show file tree
Hide file tree
Showing 11 changed files with 519 additions and 49 deletions.
82 changes: 47 additions & 35 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,7 +109,7 @@ function mergeItems(itemType, currentItem, newItem, ignoreLocations) {
return currentItem;
}

function subscribeOnParserEvents(parser, options, version, resolve, reject) {
function subscribeOnParserEvents(parser, ignoredVisibilities, version, resolve, reject) {
const component = {
version: version
};
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 version = options.version || SvelteVersionDetector.detectVersionFromStructure(options.structure, options.defaultVersion);

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

subscribeOnParserEvents(parser, options, version, resolve, reject);
subscribeOnParserEvents(parser, options.ignoredVisibilities, version, 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?: HtmlBlock[], styles?: HtmlBlock[] }} FileStructure
* @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);
}

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,
};
2 changes: 1 addition & 1 deletion lib/parser.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ const path = require('path');
const utils = require('./utils');
const jsdoc = require('./jsdoc');

const hasOwnProperty = (obj, prop) => Object.prototype.hasOwnProperty.call(obj, prop);
const hasOwnProperty = utils.hasOwnProperty;

const DEFAULT_OPTIONS = {
/**
Expand Down
Loading

0 comments on commit 3bcb993

Please sign in to comment.