temp - big rewrite
This commit is contained in:
parent
e78e17725e
commit
5d3f06ebef
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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);
|
||||
|
|
|
@ -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>;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
title: Gome's world
|
||||
color: red
|
||||
|
|
50
trellis.ts
50
trellis.ts
|
@ -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 });
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue