temp - big rewrite

This commit is contained in:
gome 2022-05-08 13:48:31 -05:00
parent e78e17725e
commit 5d3f06ebef
7 changed files with 257 additions and 62 deletions

138
extension.ts Normal file
View File

@ -0,0 +1,138 @@
import { FileAccess } from "./file-access.ts";
import { InputContext } from './input-context.ts';
import { Replacer } from './replacer.ts';
import * as path from 'https://deno.land/std@0.138.0/path/mod.ts';
type OutputInfo = {
html?: {
filename: string;
content: string;
};
copy?: {
filename: string;
to: string;
};
} | undefined;
export type ExtensionReturn = {
/// wait a minute... can content/metadata be modified in these?
deps?: Dependency[];
getOutput: () => OutputInfo;
}
type DependencyContext = { input: InputContext, output: OutputInfo };
type Dependency = { filename: string, onContext: (context: DependencyContext) => Dependency[] };
type Extension = {
process(context: InputContext, template: Replacer, getSnippet: (name: string) => Promise<Replacer | undefined>): Promise<ExtensionReturn>;
defaultTemplate?: string;
}
const extensions: { [key: string]: Extension|undefined; readonly [key: symbol]: Extension } = {
[Symbol('default')]: { process: async () => ({ getOutput: () => undefined }) }
};
export class Plant {
dependencies: { dependency: Dependency, history: string[] }[] = [];
input: InputContext;
extension: Extension;
// info about what to output
output: undefined;
getOutput: (() => OutputInfo)|undefined = undefined;
// TODO: MERGE createPlant and process, move it outside class def, make the class entirely private to module
static async createPlant(input: InputContext) {
const { filename, getMetadata } = input;
const metadata = await getMetadata();
return new Plant(input, metadata?.getString('extension') ?? path.extname(filename));
}
private constructor(input: InputContext, extension_name: string) {
this.input = input;
const extension = extensions[extension_name];
this.extension = extension ?? extensions[Symbol('default')];
}
// this shouldn't actually depend on a reference to file-access.ts
async process(getReplacer: FileAccess['getReplacer']): Promise<{
resolve: Plant['resolveDependencies'];
context?: undefined;
} | {
resolve?: undefined;
context: DependencyContext;
}> {
const { getMetadata } = this.input;
const metadata = await getMetadata();
const templateName = metadata?.getString('template') ?? this.extension.defaultTemplate ?? 'default';
const template = await getReplacer(templateName, 'template');
if (template) {
const extensionResult = await this.extension.process(this.input, template, (name) => getReplacer(name, 'snippet'));
if (extensionResult.deps?.length) {
this.dependencies.push(...extensionResult.deps.map(dependency => ({ dependency, history: [] })));
}
this.getOutput = extensionResult.getOutput;
// if (metadata?.getBoolean('output-html') !== false) {
// let outputFilename = filename;
// const htmlFilename = metadata?.getString('html-filename');
// if (htmlFilename) {
// outputFilename.replace(path.basename(outputFilename), htmlFilename);
// }
// outputFilename += '.html';
// await fileAccess.outputFile(outputFilename, output);
// }
// if (metadata?.getString('output-original')) {
// // what do when
// Deno.copy
// }
}
// NOTE: if template wasn't found, this plant has no dependencies and no output
return this.resolveDependencies({});
}
async resolveDependencies(contexts: { [key: string]: DependencyContext }): Promise<{
resolve: Plant['resolveDependencies'];
context?: undefined;
} | {
resolve?: undefined;
context: DependencyContext;
}> {
for (const [name, context] of Object.entries(contexts)) {
// dependencies that match this name
const matches = this.dependencies.filter(entry => entry.dependency.filename === name);
if (matches.length) {
// push results onto dependency list
this.dependencies.concat(
// process each match
matches.flatMap(entry =>
// acutal callback invoked
entry.dependency.onContext(context)
// returns a list of dependencies, pair those with updated history
.map(dependency => ({
dependency,
history: [...entry.history, entry.dependency.filename],
}))
)
);
}
// if all dependencies have been processed, we are done
if (this.dependencies.length === 0) {
return { context: { output: this.getOutput?.(), input: this.input } };
}
}
// this finds duplicates in the dependencies' history
const hasCycle = this.dependencies.find(entry => (new Set(entry.history)).size !== entry.history.length);
if (hasCycle) {
console.error(`Dependency cycle detected in ${this.input.filename}: [${hasCycle.history.join(', ')}]`);
// stop trying to resolve dependencies
return { context: { output: this.getOutput?.(), input: this.input } };
}
// have not resolved all dependencies
return { resolve: this.resolveDependencies };
}
static registerExtensions(...new_extensions: [string, Extension][]) {
Object.assign(extensions, Object.fromEntries(new_extensions));
}
}

View File

@ -2,14 +2,13 @@ import * as path from 'https://deno.land/std@0.138.0/path/mod.ts';
import { backupDefaultTemplate } from './backup-defaults.ts';
import { createReplacer, Replacer } from './replacer.ts';
import * as YAML from 'https://deno.land/std@0.138.0/encoding/yaml.ts';
import { InputContext } from "./input-context.ts";
import { InputContext } from './input-context.ts';
import { Metadata } from './metadata.ts';
export interface FileAccess {
getReplacer: (name: string, type: 'template' | 'snippet') => Promise<Replacer | undefined>;
inputFiles: AsyncGenerator<InputContext, void, undefined>;
// TEMP
outputDir: string;
inputDir: string;
inputFiles: Generator<InputContext, void, undefined>;
outputFile: (filename: string, output: string) => Promise<void>;
}
export type CreateFileAccessOptions = {
@ -87,41 +86,28 @@ export async function createFileAccess({
}
}
},
inputFiles: (async function* () {
inputFiles: (function* () {
for (const filename of inputIndex) {
if (path.extname(filename) === '.yml') {
// skip YAML files, they are configuration
continue;
}
const content = await readFile(filename)
.catch(e => {
console.warn(`Warning: ${e}\n skipping ${filename}`);
return undefined;
});
if (!content) {
continue;
}
const metadata = await readFile(filename + '.yml')
.then(content => content ? YAML.parse(content) : undefined)
.then(metadata => {
if (typeof metadata === 'object') {
return metadata as { [key: string|number]: unknown };
} else {
throw `metadata file ${filename + '.yml'} is not a valid config file (must parse as an object)`;
}
})
.catch(e => {
if (!(e instanceof Deno.errors.NotFound)) {
console.warn(`Warning: ${e}`);
}
return undefined;
});
// yield for async generator
yield { filename, content, metadata };
// yield for generator
yield { filename, getContent: deferredCachedFileReader(filename), getMetadata: deferredCachedMetadataReader(filename) };
}
})(), // IEFF to create async generator
outputDir,
inputDir,
outputFile: async (filename: string, output: string) => {
try {
filename = filename.replace(inputDir, outputDir);
const directory = path.dirname(filename);
if (!(await doesDirectoryExist(directory))) {
createDirectory(directory);
}
await writeFile(filename, output);
} catch (e) {
console.warn(`Warning: ${e}`);
}
}
}
});
}
@ -165,7 +151,7 @@ async function createDirectory(dirname: string, replace?: boolean) {
}
});
}
return Deno.mkdir(dirname);
return Deno.mkdir(dirname, { recursive: true });
} catch (e) {
throw `failed to create directory "${dirname}": ${e.name} (${e.code})`;
}
@ -194,6 +180,7 @@ async function readDirectory(dirname: string, options?: { recursive?: boolean; e
}
}
// not everything is a text file!!!! rewrite time!!!
async function readFile(filename: string) {
try {
return Deno.readTextFile(filename);
@ -202,6 +189,50 @@ async function readFile(filename: string) {
}
}
function deferredCachedFileReader(filename: string) {
let cached: string|undefined = undefined;
return async () => {
return cached ?? (cached = await readFile(filename));
}
}
function deferredCachedMetadataReader(filename: string) {
let cached: Metadata|undefined = undefined;
let attemptedRead = false;
return async () => {
if (attemptedRead) {
return cached;
} else {
return cached = await readFile(filename + '.yml')
.then(content => content ? YAML.parse(content) : undefined)
.then(metadata => {
if (typeof metadata === 'object' && metadata) {
return new Metadata(metadata);
} else {
throw `metadata file ${filename + '.yml'} is not a valid config file (must parse as an object)`;
}
})
.catch(e => {
if (!(e instanceof Deno.errors.NotFound)) {
console.warn(`Warning: ${e}`);
}
return undefined;
})
.finally(() => {
attemptedRead = true;
});
}
}
}
async function writeFile(filename: string, output: string) {
try {
return Deno.writeTextFile(filename, output);
} catch (e) {
throw `failed to write "${filename}": ${e.name} (${e.code})`;
}
}
async function doesFileExist(filename: string) {
try {
const stat = await Deno.stat(filename);

View File

@ -1,5 +1,7 @@
import { Metadata } from './metadata.ts';
export interface InputContext {
filename: string;
content: string;
metadata?: { [key: string|number]: unknown };
getContent: () => Promise<string>;
getMetadata: () => Promise<Metadata|undefined>;
}

21
metadata.ts Normal file
View File

@ -0,0 +1,21 @@
export class Metadata {
dictionary: { [key: string|number]: unknown };
constructor(dictionary: {}) {
this.dictionary = dictionary;
}
getString(key: string|number): string|undefined {
const value = this.dictionary[key];
return typeof value === 'string'
? value
: undefined;
}
getBoolean(key: string|number): boolean|undefined {
const value = this.dictionary[key];
return typeof value === 'boolean'
? value
: undefined;
}
}

View File

@ -1,5 +1,7 @@
export type ReplacerArgs = { [key: string]: string };
export interface Replacer {
(args: { [key: string]: string }): string;
(args: ReplacerArgs): string;
}
export function createReplacer(template: string): Replacer {

View File

@ -1 +1,2 @@
title: Gome's world
color: red

View File

@ -1,8 +1,9 @@
import * as path from 'https://deno.land/std@0.138.0/path/mod.ts';
import { parse } from 'https://deno.land/std@0.138.0/flags/mod.ts';
import { createFileAccess, FileAccess } from "./file-access.ts";
import { InputContext } from "./input-context.ts";
import { Replacer } from "./replacer.ts";
import { createFileAccess, FileAccess } from './file-access.ts';
import { InputContext } from './input-context.ts';
import { Replacer } from './replacer.ts';
import { Plant } from './extension.ts';
let args = parse(Deno.args);
@ -16,31 +17,30 @@ const fileAccess = await createFileAccess({
output,
});
interface Extensions {
[key: string]: (context: InputContext, getSnippet: (name: string) => Promise<Replacer | undefined>) => { [key: string]: string };
}
const extensions: Extensions = {
'.txt': ({content, metadata}) => {
const color = typeof metadata?.color === 'string' ? metadata.color : undefined;
const extensions: { [key: string]: Plant | undefined; [key: symbol]: undefined } = {
'.txt': async ({getContent, getMetadata}) => {
const content = await getContent();
const metadata = await getMetadata();
const color = metadata?.getString('color');
return { content: `<main${color ? ` style="color: ${color}"` : ''}>${content}</main>` };
}
};
for await (const input of fileAccess.inputFiles) {
const { filename, metadata } = input;
const templateName = typeof metadata?.template === 'string'
? metadata.template
: 'default';
const template = await fileAccess.getReplacer(templateName, 'template');
if (template) {
const extension = extensions[path.extname(filename)];
if (extension as unknown) {
const output = template(extension(input, (name) => fileAccess.getReplacer(name, 'snippet')));
// TODO: put this in file access and let it create parent directories if missing
await Deno.writeTextFile(filename.replace(fileAccess.inputDir, fileAccess.outputDir) + '.html', output);
} else {
console.info(filename);
}
const resolved = [];
const unresolved = [];
for (const input of fileAccess.inputFiles) {
const new_plant = await Plant.createPlant(input);
const { filename, getMetadata } = input;
const metadata = await getMetadata();
const extension = extensions[metadata?.getString('extension') ?? path.extname(filename)];
extension?.process(input, fileAccess.getReplacer);
if (extension) {
files.push({ filename, extension });
}
}