refactor application.yaml to become an os definition #40
This commit is contained in:
43
src/application/Common/Enum.ts
Normal file
43
src/application/Common/Enum.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
// Because we cannot do "T extends enum" 😞 https://github.com/microsoft/TypeScript/issues/30611
|
||||
type EnumType = number | string;
|
||||
type EnumVariable<T extends EnumType, TEnumValue extends EnumType> = { [key in T]: TEnumValue };
|
||||
|
||||
export interface IEnumParser<TEnum> {
|
||||
parseEnum(value: string, propertyName: string): TEnum;
|
||||
}
|
||||
export function createEnumParser<T extends EnumType, TEnumValue extends EnumType>(
|
||||
enumVariable: EnumVariable<T, TEnumValue>): IEnumParser<TEnumValue> {
|
||||
return {
|
||||
parseEnum: (value, propertyName) => parseEnumValue(value, propertyName, enumVariable),
|
||||
};
|
||||
}
|
||||
function parseEnumValue<T extends EnumType, TEnumValue extends EnumType>(
|
||||
value: string,
|
||||
enumName: string,
|
||||
enumVariable: EnumVariable<T, TEnumValue>): TEnumValue {
|
||||
if (!value) {
|
||||
throw new Error(`undefined ${enumName}`);
|
||||
}
|
||||
if (typeof value !== 'string') {
|
||||
throw new Error(`unexpected type of ${enumName}: "${typeof value}"`);
|
||||
}
|
||||
const casedValue = getEnumNames(enumVariable)
|
||||
.find((enumValue) => enumValue.toLowerCase() === value.toLowerCase());
|
||||
if (!casedValue) {
|
||||
throw new Error(`unknown ${enumName}: "${value}"`);
|
||||
}
|
||||
return enumVariable[casedValue as keyof typeof enumVariable];
|
||||
}
|
||||
|
||||
export function getEnumNames<T extends EnumType, TEnumValue extends EnumType>(
|
||||
enumVariable: EnumVariable<T, TEnumValue>): string[] {
|
||||
return Object
|
||||
.values(enumVariable)
|
||||
.filter((enumMember) => typeof enumMember === 'string') as string[];
|
||||
}
|
||||
|
||||
export function getEnumValues<T extends EnumType, TEnumValue extends EnumType>(
|
||||
enumVariable: EnumVariable<T, TEnumValue>): TEnumValue[] {
|
||||
return getEnumNames(enumVariable)
|
||||
.map((level) => enumVariable[level]) as TEnumValue[];
|
||||
}
|
||||
@@ -1,14 +1,18 @@
|
||||
import { Category } from '@/domain/Category';
|
||||
import { Application } from '@/domain/Application';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||
import { ApplicationYaml } from 'js-yaml-loader!./../application.yaml';
|
||||
import { YamlApplication } from 'js-yaml-loader!./../application.yaml';
|
||||
import { parseCategory } from './CategoryParser';
|
||||
import { ProjectInformation } from '@/domain/ProjectInformation';
|
||||
import { parseProjectInformation } from './ProjectInformationParser';
|
||||
import { ScriptCompiler } from './Compiler/ScriptCompiler';
|
||||
import { OperatingSystem } from '@/domain/OperatingSystem';
|
||||
import { parseScriptingDefinition } from './ScriptingDefinitionParser';
|
||||
import { createEnumParser } from '../Common/Enum';
|
||||
|
||||
|
||||
export function parseApplication(content: ApplicationYaml, env: NodeJS.ProcessEnv = process.env): IApplication {
|
||||
export function parseApplication(
|
||||
content: YamlApplication,
|
||||
env: NodeJS.ProcessEnv = process.env,
|
||||
osParser = createEnumParser(OperatingSystem)): IApplication {
|
||||
validate(content);
|
||||
const compiler = new ScriptCompiler(content.functions);
|
||||
const categories = new Array<Category>();
|
||||
@@ -16,23 +20,18 @@ export function parseApplication(content: ApplicationYaml, env: NodeJS.ProcessEn
|
||||
const category = parseCategory(action, compiler);
|
||||
categories.push(category);
|
||||
}
|
||||
const info = readAppInformation(env);
|
||||
const os = osParser.parseEnum(content.os, 'os');
|
||||
const info = parseProjectInformation(env);
|
||||
const scripting = parseScriptingDefinition(content.scripting, info);
|
||||
const app = new Application(
|
||||
os,
|
||||
info,
|
||||
categories);
|
||||
categories,
|
||||
scripting);
|
||||
return app;
|
||||
}
|
||||
|
||||
function readAppInformation(environment: NodeJS.ProcessEnv): IProjectInformation {
|
||||
return new ProjectInformation(
|
||||
environment.VUE_APP_NAME,
|
||||
environment.VUE_APP_VERSION,
|
||||
environment.VUE_APP_REPOSITORY_URL,
|
||||
environment.VUE_APP_HOMEPAGE_URL,
|
||||
);
|
||||
}
|
||||
|
||||
function validate(content: ApplicationYaml): void {
|
||||
function validate(content: YamlApplication): void {
|
||||
if (!content) {
|
||||
throw new Error('application is null or undefined');
|
||||
}
|
||||
|
||||
73
src/application/Parser/Compiler/ILCode.ts
Normal file
73
src/application/Parser/Compiler/ILCode.ts
Normal file
@@ -0,0 +1,73 @@
|
||||
export interface IILCode {
|
||||
compile(): string;
|
||||
getUniqueParameterNames(): string[];
|
||||
substituteParameter(parameterName: string, parameterValue: string): IILCode;
|
||||
}
|
||||
|
||||
export function generateIlCode(rawText: string): IILCode {
|
||||
const ilCode = generateIl(rawText);
|
||||
return new ILCode(ilCode);
|
||||
}
|
||||
|
||||
class ILCode implements IILCode {
|
||||
private readonly ilCode: string;
|
||||
|
||||
constructor(ilCode: string) {
|
||||
this.ilCode = ilCode;
|
||||
}
|
||||
|
||||
public substituteParameter(parameterName: string, parameterValue: string): IILCode {
|
||||
const newCode = substituteParameter(this.ilCode, parameterName, parameterValue);
|
||||
return new ILCode(newCode);
|
||||
}
|
||||
|
||||
public getUniqueParameterNames(): string[] {
|
||||
return getUniqueParameterNames(this.ilCode);
|
||||
}
|
||||
|
||||
public compile(): string {
|
||||
ensureNoExpressionLeft(this.ilCode);
|
||||
return this.ilCode;
|
||||
}
|
||||
}
|
||||
|
||||
// Trim each expression and put them inside "{{exp|}}" e.g. "{{ $hello }}" becomes "{{exp|$hello}}"
|
||||
function generateIl(rawText: string): string {
|
||||
return rawText.replace(/\{\{([\s]*[^;\s\{]+[\s]*)\}\}/g, (_, match) => {
|
||||
return `\{\{exp|${match.trim()}\}\}`;
|
||||
});
|
||||
}
|
||||
|
||||
// finds all "{{exp|..}} left"
|
||||
function ensureNoExpressionLeft(ilCode: string) {
|
||||
const allSubstitutions = ilCode.matchAll(/\{\{exp\|(.*?)\}\}/g);
|
||||
const allMatches = Array.from(allSubstitutions, (match) => match[1]);
|
||||
const uniqueExpressions = getDistinctValues(allMatches);
|
||||
if (uniqueExpressions.length > 0) {
|
||||
throw new Error(`unknown expression: ${printList(uniqueExpressions)}`);
|
||||
}
|
||||
}
|
||||
|
||||
// Parses all distinct usages of {{exp|$parameterName}}
|
||||
function getUniqueParameterNames(ilCode: string) {
|
||||
const allSubstitutions = ilCode.matchAll(/\{\{exp\|\$([^;\s\{]+[\s]*)\}\}/g);
|
||||
const allParameters = Array.from(allSubstitutions, (match) => match[1]);
|
||||
const uniqueParameterNames = getDistinctValues(allParameters);
|
||||
return uniqueParameterNames;
|
||||
}
|
||||
|
||||
// substitutes {{exp|$parameterName}} to value of the parameter
|
||||
function substituteParameter(ilCode: string, parameterName: string, parameterValue: string) {
|
||||
const pattern = `{{exp|$${parameterName}}}`;
|
||||
return ilCode.split(pattern).join(parameterValue); // as .replaceAll() is not yet supported by TS
|
||||
}
|
||||
|
||||
function getDistinctValues(values: readonly string[]): string[] {
|
||||
return values.filter((value, index, self) => {
|
||||
return self.indexOf(value) === index;
|
||||
});
|
||||
}
|
||||
|
||||
function printList(list: readonly string[]): string {
|
||||
return `"${list.join('","')}"`;
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { generateIlCode, IILCode } from './ILCode';
|
||||
import { IScriptCode } from '@/domain/IScriptCode';
|
||||
import { ScriptCode } from '@/domain/ScriptCode';
|
||||
import { YamlScript, YamlFunction, FunctionCall, ScriptFunctionCall, FunctionCallParameters } from 'js-yaml-loader!./application.yaml';
|
||||
@@ -125,20 +126,22 @@ function compileCode(func: YamlFunction, parameters: FunctionCallParameters): IC
|
||||
}
|
||||
|
||||
function compileExpressions(code: string, parameters: FunctionCallParameters): string {
|
||||
let intermediateCode = compileToIL(code);
|
||||
let intermediateCode = generateIlCode(code);
|
||||
intermediateCode = substituteParameters(intermediateCode, parameters);
|
||||
ensureNoExpressionLeft(intermediateCode);
|
||||
return intermediateCode;
|
||||
return intermediateCode.compile();
|
||||
}
|
||||
|
||||
function substituteParameters(intermediateCode: string, parameters: FunctionCallParameters): string {
|
||||
const parameterNames = getUniqueParameterNamesFromIL(intermediateCode);
|
||||
function substituteParameters(intermediateCode: IILCode, parameters: FunctionCallParameters): IILCode {
|
||||
const parameterNames = intermediateCode.getUniqueParameterNames();
|
||||
if (parameterNames.length && !parameters) {
|
||||
throw new Error(`no parameters defined, expected: ${printList(parameterNames)}`);
|
||||
}
|
||||
for (const parameterName of parameterNames) {
|
||||
const parameterValue = parameters[parameterName];
|
||||
intermediateCode = substituteParameter(intermediateCode, parameterName, parameterValue);
|
||||
if (!parameterValue) {
|
||||
throw Error(`parameter value is not provided for "${parameterName}" in function call`);
|
||||
}
|
||||
intermediateCode = intermediateCode.substituteParameter(parameterName, parameterValue);
|
||||
}
|
||||
return intermediateCode;
|
||||
}
|
||||
@@ -158,43 +161,3 @@ function getCallSequence(call: ScriptFunctionCall): FunctionCall[] {
|
||||
}
|
||||
return [ call as FunctionCall ];
|
||||
}
|
||||
|
||||
function getDistinctValues(values: readonly string[]): string[] {
|
||||
return values.filter((value, index, self) => {
|
||||
return self.indexOf(value) === index;
|
||||
});
|
||||
}
|
||||
|
||||
// Trim each expression and put them inside "{{exp|}}" e.g. "{{ $hello }}" becomes "{{exp|$hello}}"
|
||||
function compileToIL(code: string) {
|
||||
return code.replace(/\{\{([\s]*[^;\s\{]+[\s]*)\}\}/g, (_, match) => {
|
||||
return `\{\{exp|${match.trim()}\}\}`;
|
||||
});
|
||||
}
|
||||
|
||||
// Parses all distinct usages of {{exp|$parameterName}}
|
||||
function getUniqueParameterNamesFromIL(ilCode: string) {
|
||||
const allSubstitutions = ilCode.matchAll(/\{\{exp\|\$([^;\s\{]+[\s]*)\}\}/g);
|
||||
const allParameters = Array.from(allSubstitutions, (match) => match[1]);
|
||||
const uniqueParameterNames = getDistinctValues(allParameters);
|
||||
return uniqueParameterNames;
|
||||
}
|
||||
|
||||
// substitutes {{exp|$parameterName}} to value of the parameter
|
||||
function substituteParameter(ilCode: string, parameterName: string, parameterValue: string) {
|
||||
if (!parameterValue) {
|
||||
throw Error(`parameter value is not provided for "${parameterName}" in function call`);
|
||||
}
|
||||
const pattern = `{{exp|$${parameterName}}}`;
|
||||
return ilCode.split(pattern).join(parameterValue); // as .replaceAll() is not yet supported by TS
|
||||
}
|
||||
|
||||
// finds all "{{exp|..}} left"
|
||||
function ensureNoExpressionLeft(ilCode: string) {
|
||||
const allSubstitutions = ilCode.matchAll(/\{\{exp\|(.*?)\}\}/g);
|
||||
const allMatches = Array.from(allSubstitutions, (match) => match[1]);
|
||||
const uniqueExpressions = getDistinctValues(allMatches);
|
||||
if (uniqueExpressions.length > 0) {
|
||||
throw new Error(`unknown expression: ${printList(uniqueExpressions)}`);
|
||||
}
|
||||
}
|
||||
|
||||
12
src/application/Parser/ProjectInformationParser.ts
Normal file
12
src/application/Parser/ProjectInformationParser.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||
import { ProjectInformation } from '@/domain/ProjectInformation';
|
||||
|
||||
export function parseProjectInformation(
|
||||
environment: NodeJS.ProcessEnv): IProjectInformation {
|
||||
return new ProjectInformation(
|
||||
environment.VUE_APP_NAME,
|
||||
environment.VUE_APP_VERSION,
|
||||
environment.VUE_APP_REPOSITORY_URL,
|
||||
environment.VUE_APP_HOMEPAGE_URL,
|
||||
);
|
||||
}
|
||||
@@ -1,37 +1,32 @@
|
||||
import { Script } from '@/domain/Script';
|
||||
import { YamlScript } from 'js-yaml-loader!./application.yaml';
|
||||
import { parseDocUrls } from './DocumentationParser';
|
||||
import { RecommendationLevelNames, RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||
import { RecommendationLevel } from '@/domain/RecommendationLevel';
|
||||
import { IScriptCompiler } from './Compiler/IScriptCompiler';
|
||||
import { IScriptCode } from '@/domain/IScriptCode';
|
||||
import { ScriptCode } from '@/domain/ScriptCode';
|
||||
import { createEnumParser, IEnumParser } from '../Common/Enum';
|
||||
|
||||
export function parseScript(yamlScript: YamlScript, compiler: IScriptCompiler): Script {
|
||||
export function parseScript(
|
||||
yamlScript: YamlScript, compiler: IScriptCompiler,
|
||||
levelParser = createEnumParser(RecommendationLevel)): Script {
|
||||
validateScript(yamlScript);
|
||||
if (!compiler) {
|
||||
throw new Error('undefined compiler');
|
||||
}
|
||||
const script = new Script(
|
||||
/* name */ yamlScript.name,
|
||||
/* code */ parseCode(yamlScript, compiler),
|
||||
/* docs */ parseDocUrls(yamlScript),
|
||||
/* level */ getLevel(yamlScript.recommend));
|
||||
/* name */ yamlScript.name,
|
||||
/* code */ parseCode(yamlScript, compiler),
|
||||
/* docs */ parseDocUrls(yamlScript),
|
||||
/* level */ parseLevel(yamlScript.recommend, levelParser));
|
||||
return script;
|
||||
}
|
||||
|
||||
function getLevel(level: string): RecommendationLevel | undefined {
|
||||
function parseLevel(level: string, parser: IEnumParser<RecommendationLevel>): RecommendationLevel | undefined {
|
||||
if (!level) {
|
||||
return undefined;
|
||||
}
|
||||
if (typeof level !== 'string') {
|
||||
throw new Error(`level must be a string but it was ${typeof level}`);
|
||||
}
|
||||
const typedLevel = RecommendationLevelNames
|
||||
.find((l) => l.toLowerCase() === level.toLowerCase());
|
||||
if (!typedLevel) {
|
||||
throw new Error(`unknown level: \"${level}\"`);
|
||||
}
|
||||
return RecommendationLevel[typedLevel as keyof typeof RecommendationLevel];
|
||||
return parser.parseEnum(level, 'level');
|
||||
}
|
||||
|
||||
function parseCode(yamlScript: YamlScript, compiler: IScriptCompiler): IScriptCode {
|
||||
|
||||
36
src/application/Parser/ScriptingDefinitionParser.ts
Normal file
36
src/application/Parser/ScriptingDefinitionParser.ts
Normal file
@@ -0,0 +1,36 @@
|
||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
import { YamlScriptingDefinition } from 'js-yaml-loader!./application.yaml';
|
||||
import { ScriptingDefinition } from '@/domain/ScriptingDefinition';
|
||||
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
|
||||
import { IProjectInformation } from '@/domain/IProjectInformation';
|
||||
import { createEnumParser } from '../Common/Enum';
|
||||
import { generateIlCode } from './Compiler/ILCode';
|
||||
|
||||
export function parseScriptingDefinition(
|
||||
definition: YamlScriptingDefinition,
|
||||
info: IProjectInformation,
|
||||
date = new Date(),
|
||||
languageParser = createEnumParser(ScriptingLanguage)): IScriptingDefinition {
|
||||
if (!info) {
|
||||
throw new Error('undefined info');
|
||||
}
|
||||
if (!definition) {
|
||||
throw new Error('undefined definition');
|
||||
}
|
||||
const language = languageParser.parseEnum(definition.language, 'language');
|
||||
const startCode = applySubstitutions(definition.startCode, info, date);
|
||||
const endCode = applySubstitutions(definition.endCode, info, date);
|
||||
return new ScriptingDefinition(
|
||||
language,
|
||||
startCode,
|
||||
endCode,
|
||||
);
|
||||
}
|
||||
|
||||
function applySubstitutions(code: string, info: IProjectInformation, date: Date): string {
|
||||
let ilCode = generateIlCode(code);
|
||||
ilCode = ilCode.substituteParameter('homepage', info.homepage);
|
||||
ilCode = ilCode.substituteParameter('version', info.version);
|
||||
ilCode = ilCode.substituteParameter('date', date.toUTCString());
|
||||
return ilCode.compile();
|
||||
}
|
||||
20
src/application/State/ApplicationContext.ts
Normal file
20
src/application/State/ApplicationContext.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { IApplicationContext } from './IApplicationContext';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { IApplicationState } from './IApplicationState';
|
||||
import { ApplicationState } from './ApplicationState';
|
||||
import applicationFile from 'js-yaml-loader!@/application/application.yaml';
|
||||
import { parseApplication } from '../Parser/ApplicationParser';
|
||||
|
||||
|
||||
export function createContext(): IApplicationContext {
|
||||
const application = parseApplication(applicationFile);
|
||||
const context = new ApplicationContext(application);
|
||||
return context;
|
||||
}
|
||||
|
||||
export class ApplicationContext implements IApplicationContext {
|
||||
public readonly state: IApplicationState;
|
||||
public constructor(public readonly app: IApplication) {
|
||||
this.state = new ApplicationState(app);
|
||||
}
|
||||
}
|
||||
9
src/application/State/ApplicationContextProvider.ts
Normal file
9
src/application/State/ApplicationContextProvider.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
import { ApplicationContext } from './ApplicationContext';
|
||||
import { IApplicationContext } from '@/application/State/IApplicationContext';
|
||||
import applicationFile from 'js-yaml-loader!@/application/application.yaml';
|
||||
import { parseApplication } from '@/application/Parser/ApplicationParser';
|
||||
|
||||
export function buildContext(): IApplicationContext {
|
||||
const application = parseApplication(applicationFile);
|
||||
return new ApplicationContext(application);
|
||||
}
|
||||
@@ -3,43 +3,18 @@ import { IUserFilter } from './Filter/IUserFilter';
|
||||
import { ApplicationCode } from './Code/ApplicationCode';
|
||||
import { UserSelection } from './Selection/UserSelection';
|
||||
import { IUserSelection } from './Selection/IUserSelection';
|
||||
import { AsyncLazy } from '@/infrastructure/Threading/AsyncLazy';
|
||||
import { Signal } from '@/infrastructure/Events/Signal';
|
||||
import { parseApplication } from '../Parser/ApplicationParser';
|
||||
import { IApplicationState } from './IApplicationState';
|
||||
import { Script } from '@/domain/Script';
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { IApplicationCode } from './Code/IApplicationCode';
|
||||
import applicationFile from 'js-yaml-loader!@/application/application.yaml';
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
|
||||
/** Mutatable singleton application state that's the single source of truth throughout the application */
|
||||
export class ApplicationState implements IApplicationState {
|
||||
/** Get singleton application state */
|
||||
public static GetAsync(): Promise<IApplicationState> {
|
||||
return ApplicationState.instance.getValueAsync();
|
||||
}
|
||||
|
||||
/** Application instance with all scripts. */
|
||||
private static instance = new AsyncLazy<IApplicationState>(() => {
|
||||
const application = parseApplication(applicationFile);
|
||||
const selectedScripts = new Array<Script>();
|
||||
const state = new ApplicationState(application, selectedScripts);
|
||||
return Promise.resolve(state);
|
||||
});
|
||||
|
||||
public readonly code: IApplicationCode;
|
||||
public readonly stateChanged = new Signal<IApplicationState>();
|
||||
public readonly selection: IUserSelection;
|
||||
public readonly filter: IUserFilter;
|
||||
|
||||
private constructor(
|
||||
/** Inner instance of the all scripts */
|
||||
public readonly app: IApplication,
|
||||
/** Initially selected scripts */
|
||||
public readonly defaultScripts: Script[]) {
|
||||
this.selection = new UserSelection(app, defaultScripts.map((script) => new SelectedScript(script, false)));
|
||||
this.code = new ApplicationCode(this.selection, app.info.version);
|
||||
public constructor(readonly app: IApplication) {
|
||||
this.selection = new UserSelection(app, []);
|
||||
this.code = new ApplicationCode(this.selection, app.scripting);
|
||||
this.filter = new UserFilter(app);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { UserScriptGenerator } from './Generation/UserScriptGenerator';
|
||||
import { Signal } from '@/infrastructure/Events/Signal';
|
||||
import { IApplicationCode } from './IApplicationCode';
|
||||
import { IUserScriptGenerator } from './Generation/IUserScriptGenerator';
|
||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
|
||||
export class ApplicationCode implements IApplicationCode {
|
||||
public readonly changed = new Signal<ICodeChangedEvent>();
|
||||
@@ -16,10 +17,10 @@ export class ApplicationCode implements IApplicationCode {
|
||||
|
||||
constructor(
|
||||
userSelection: IUserSelection,
|
||||
private readonly version: string,
|
||||
private readonly scriptingDefinition: IScriptingDefinition,
|
||||
private readonly generator: IUserScriptGenerator = new UserScriptGenerator()) {
|
||||
if (!userSelection) { throw new Error('userSelection is null or undefined'); }
|
||||
if (!version) { throw new Error('version is null or undefined'); }
|
||||
if (!scriptingDefinition) { throw new Error('scriptingDefinition is null or undefined'); }
|
||||
if (!generator) { throw new Error('generator is null or undefined'); }
|
||||
this.setCode(userSelection.selectedScripts);
|
||||
userSelection.changed.on((scripts) => {
|
||||
@@ -29,7 +30,7 @@ export class ApplicationCode implements IApplicationCode {
|
||||
|
||||
private setCode(scripts: ReadonlyArray<SelectedScript>): void {
|
||||
const oldScripts = Array.from(this.scriptPositions.keys());
|
||||
const code = this.generator.buildCode(scripts, this.version);
|
||||
const code = this.generator.buildCode(scripts, this.scriptingDefinition);
|
||||
this.current = code.code;
|
||||
this.scriptPositions = code.scriptPositions;
|
||||
const event = new CodeChangedEvent(code.code, oldScripts, code.scriptPositions);
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { ICodeBuilder } from './ICodeBuilder';
|
||||
|
||||
const NewLine = '\n';
|
||||
const TotalFunctionSeparatorChars = 58;
|
||||
|
||||
export class CodeBuilder {
|
||||
export class CodeBuilder implements ICodeBuilder {
|
||||
private readonly lines = new Array<string>();
|
||||
|
||||
// Returns current line starting from 0 (no lines), or 1 (have single line)
|
||||
@@ -54,7 +56,7 @@ export class CodeBuilder {
|
||||
return this
|
||||
.appendTrailingHyphensCommentLine()
|
||||
.appendCommentLine(firstHyphens + sectionName + secondHyphens)
|
||||
.appendTrailingHyphensCommentLine();
|
||||
.appendTrailingHyphensCommentLine(TotalFunctionSeparatorChars);
|
||||
}
|
||||
|
||||
public toString(): string {
|
||||
|
||||
9
src/application/State/Code/Generation/ICodeBuilder.ts
Normal file
9
src/application/State/Code/Generation/ICodeBuilder.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
export interface ICodeBuilder {
|
||||
currentLine: number;
|
||||
appendLine(code?: string): ICodeBuilder;
|
||||
appendTrailingHyphensCommentLine(totalRepeatHyphens: number): ICodeBuilder;
|
||||
appendCommentLine(commentLine?: string): ICodeBuilder;
|
||||
appendCommentLineWithHyphensAround(sectionName: string, totalRepeatHyphens: number): ICodeBuilder;
|
||||
appendFunction(name: string, code: string): ICodeBuilder;
|
||||
toString(): string;
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
import { SelectedScript } from '@/application/State/Selection/SelectedScript';
|
||||
import { IUserScript } from './IUserScript';
|
||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
|
||||
export interface IUserScriptGenerator {
|
||||
buildCode(
|
||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||
version: string): IUserScript;
|
||||
scriptingDefinition: IScriptingDefinition): IUserScript;
|
||||
}
|
||||
|
||||
@@ -4,55 +4,54 @@ import { CodeBuilder } from './CodeBuilder';
|
||||
import { ICodePosition } from '@/application/State/Code/Position/ICodePosition';
|
||||
import { CodePosition } from '../Position/CodePosition';
|
||||
import { IUserScript } from './IUserScript';
|
||||
|
||||
export const adminRightsScript = {
|
||||
name: 'Ensure admin privileges',
|
||||
code: 'fltmc >nul 2>&1 || (\n' +
|
||||
' echo Administrator privileges are required.\n' +
|
||||
' PowerShell Start -Verb RunAs \'%0\' 2> nul || (\n' +
|
||||
' echo Right-click on the script and select "Run as administrator".\n' +
|
||||
' pause & exit 1\n' +
|
||||
' )\n' +
|
||||
' exit 0\n' +
|
||||
')',
|
||||
};
|
||||
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
|
||||
import { ICodeBuilder } from './ICodeBuilder';
|
||||
|
||||
export class UserScriptGenerator implements IUserScriptGenerator {
|
||||
public buildCode(selectedScripts: ReadonlyArray<SelectedScript>, version: string): IUserScript {
|
||||
if (!selectedScripts) { throw new Error('scripts is undefined'); }
|
||||
if (!version) { throw new Error('version is undefined'); }
|
||||
constructor(private readonly codeBuilderFactory: () => ICodeBuilder = () => new CodeBuilder()) {
|
||||
|
||||
}
|
||||
public buildCode(
|
||||
selectedScripts: ReadonlyArray<SelectedScript>,
|
||||
scriptingDefinition: IScriptingDefinition): IUserScript {
|
||||
if (!selectedScripts) { throw new Error('undefined scripts'); }
|
||||
if (!scriptingDefinition) { throw new Error('undefined definition'); }
|
||||
let scriptPositions = new Map<SelectedScript, ICodePosition>();
|
||||
if (!selectedScripts.length) {
|
||||
return { code: '', scriptPositions };
|
||||
}
|
||||
const builder = initializeCode(version);
|
||||
let builder = this.codeBuilderFactory();
|
||||
builder = initializeCode(scriptingDefinition.startCode, builder);
|
||||
for (const selection of selectedScripts) {
|
||||
scriptPositions = appendSelection(selection, scriptPositions, builder);
|
||||
}
|
||||
const code = finalizeCode(builder);
|
||||
const code = finalizeCode(builder, scriptingDefinition.endCode);
|
||||
return { code, scriptPositions };
|
||||
}
|
||||
}
|
||||
|
||||
function initializeCode(version: string): CodeBuilder {
|
||||
return new CodeBuilder()
|
||||
.appendLine('@echo off')
|
||||
.appendCommentLine(`https://privacy.sexy — v${version} — ${new Date().toUTCString()}`)
|
||||
.appendFunction(adminRightsScript.name, adminRightsScript.code)
|
||||
function initializeCode(startCode: string, builder: ICodeBuilder): ICodeBuilder {
|
||||
if (!startCode) {
|
||||
return builder;
|
||||
}
|
||||
return builder
|
||||
.appendLine(startCode)
|
||||
.appendLine();
|
||||
}
|
||||
|
||||
function finalizeCode(builder: CodeBuilder): string {
|
||||
function finalizeCode(builder: ICodeBuilder, endCode: string): string {
|
||||
if (!endCode) {
|
||||
return builder.toString();
|
||||
}
|
||||
return builder.appendLine()
|
||||
.appendLine('pause')
|
||||
.appendLine('exit /b 0')
|
||||
.appendLine(endCode)
|
||||
.toString();
|
||||
}
|
||||
|
||||
function appendSelection(
|
||||
selection: SelectedScript,
|
||||
scriptPositions: Map<SelectedScript, ICodePosition>,
|
||||
builder: CodeBuilder): Map<SelectedScript, ICodePosition> {
|
||||
builder: ICodeBuilder): Map<SelectedScript, ICodePosition> {
|
||||
const startPosition = builder.currentLine + 1;
|
||||
appendCode(selection, builder);
|
||||
const endPosition = builder.currentLine - 1;
|
||||
@@ -61,7 +60,7 @@ function appendSelection(
|
||||
return scriptPositions;
|
||||
}
|
||||
|
||||
function appendCode(selection: SelectedScript, builder: CodeBuilder) {
|
||||
function appendCode(selection: SelectedScript, builder: ICodeBuilder) {
|
||||
const name = selection.revert ? `${selection.script.name} (revert)` : selection.script.name;
|
||||
const scriptCode = selection.revert ? selection.script.code.revert : selection.script.code.execute;
|
||||
builder.appendFunction(name, scriptCode);
|
||||
|
||||
7
src/application/State/IApplicationContext.ts
Normal file
7
src/application/State/IApplicationContext.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import { IApplication } from '@/domain/IApplication';
|
||||
import { IApplicationState } from './IApplicationState';
|
||||
|
||||
export interface IApplicationContext {
|
||||
readonly app: IApplication;
|
||||
readonly state: IApplicationState;
|
||||
}
|
||||
@@ -1,15 +1,10 @@
|
||||
import { IApplication } from './../../domain/IApplication';
|
||||
import { IUserFilter } from './Filter/IUserFilter';
|
||||
import { IUserSelection } from './Selection/IUserSelection';
|
||||
import { ISignal } from '@/infrastructure/Events/ISignal';
|
||||
import { IApplicationCode } from './Code/IApplicationCode';
|
||||
export { IUserSelection, IApplicationCode, IUserFilter };
|
||||
|
||||
export interface IApplicationState {
|
||||
/** Event that fires when the application states changes with new application state as parameter */
|
||||
readonly code: IApplicationCode;
|
||||
readonly filter: IUserFilter;
|
||||
readonly stateChanged: ISignal<IApplicationState>;
|
||||
readonly selection: IUserSelection;
|
||||
readonly app: IApplication;
|
||||
}
|
||||
|
||||
@@ -1,4 +1,22 @@
|
||||
# Structure documented in "docs/application-file.md"
|
||||
os: windows
|
||||
scripting:
|
||||
language: batchfile
|
||||
startCode: |-
|
||||
@echo off
|
||||
:: {{ $homepage }} — v{{ $version }} — {{ $date }}
|
||||
:: Ensure admin privileges
|
||||
fltmc >nul 2>&1 || (
|
||||
echo Administrator privileges are required.
|
||||
PowerShell Start -Verb RunAs '%0' 2> nul || (
|
||||
echo Right-click on the script and select "Run as administrator".
|
||||
pause & exit 1
|
||||
)
|
||||
exit 0
|
||||
)
|
||||
endCode: |-
|
||||
pause
|
||||
exit /b 0
|
||||
actions:
|
||||
-
|
||||
category: Privacy cleanup
|
||||
|
||||
23
src/application/application.yaml.d.ts
vendored
23
src/application/application.yaml.d.ts
vendored
@@ -1,19 +1,21 @@
|
||||
declare module 'js-yaml-loader!*' {
|
||||
export interface ApplicationYaml {
|
||||
actions: ReadonlyArray<YamlCategory>;
|
||||
functions: ReadonlyArray<YamlFunction> | undefined;
|
||||
export interface YamlApplication {
|
||||
readonly os: string;
|
||||
readonly scripting: YamlScriptingDefinition;
|
||||
readonly actions: ReadonlyArray<YamlCategory>;
|
||||
readonly functions?: ReadonlyArray<YamlFunction>;
|
||||
}
|
||||
|
||||
export interface YamlCategory extends YamlDocumentable {
|
||||
children: ReadonlyArray<CategoryOrScript>;
|
||||
category: string;
|
||||
readonly children: ReadonlyArray<CategoryOrScript>;
|
||||
readonly category: string;
|
||||
}
|
||||
|
||||
export type CategoryOrScript = YamlCategory | YamlScript;
|
||||
export type DocumentationUrls = ReadonlyArray<string> | string;
|
||||
|
||||
export interface YamlDocumentable {
|
||||
docs?: DocumentationUrls;
|
||||
readonly docs?: DocumentationUrls;
|
||||
}
|
||||
|
||||
export interface YamlFunction {
|
||||
@@ -42,6 +44,13 @@ declare module 'js-yaml-loader!*' {
|
||||
recommend: string | undefined;
|
||||
}
|
||||
|
||||
const content: ApplicationYaml;
|
||||
export interface YamlScriptingDefinition {
|
||||
readonly language: string;
|
||||
readonly fileExtension: string;
|
||||
readonly startCode: string;
|
||||
readonly endCode: string;
|
||||
}
|
||||
|
||||
const content: YamlApplication;
|
||||
export default content;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user