Refactoring

This commit is contained in:
Artur Gurgul 2025-06-08 21:57:33 +02:00
parent 1e3833f7d0
commit aa563e60ea
37 changed files with 472 additions and 2392 deletions

View file

@ -1,28 +1,32 @@
import fs from 'fs'
import path from 'path'
import yaml from 'js-yaml'
import { fileURLToPath } from 'url'
export default class ProjectConfig {
public config: any = {}
public config: any
private __filename: string
private __dirname: string
constructor() {
}
public load() {
this.config = {
this.config = {
... this.read(),
buildDir: './.build',
ignore: [".build", ".sajt"]
}
this.config.remote.port = 22
this.config.remote.privateKey = fs.readFileSync(path.resolve(process.env.HOME ?? "", '.ssh/id_rsa'))
this.__filename = fileURLToPath(import.meta.url)
this.__dirname = path.dirname(this.__filename)
this.config.defaultProjectPath = path.join(this.__dirname, '..', 'empty')
}
read(): any {
const __dirname = process.cwd()
const configPath = path.join(__dirname, '.sajt/config.yaml')
if (!fs.existsSync(configPath)) {
return {}
}
const fileContents = fs.readFileSync(configPath, 'utf8')
return yaml.load(fileContents)
}

View file

@ -1,12 +1,8 @@
import { Command } from 'commander'
import chalk from 'chalk'
//import fs from 'fs-extra'
import path from 'path'
import Project from './project.js'
import { serve } from './serve.js'
const program = new Command()
let project = new Project()
program

View file

@ -1,3 +1,5 @@
import Parser , {File} from "./renderer.js"
import hljs from 'highlight.js'
import { marked } from 'marked'
import { markedHighlight } from 'marked-highlight'
@ -13,15 +15,19 @@ marked.use(markedHighlight({
}
}))
export function parseMD(file: string) {
const fileContents = fs.readFileSync(path.join("./", file), 'utf8')
export default class Markdown implements Parser {
name = "md"
loadAsHTML(filePath: string): File {
const fileContents = fs.readFileSync(path.join("./", filePath), 'utf8')
const { data: metadata, content: markdownContent } = matter(fileContents)
const htmlContent = marked(markdownContent)
const htmlContent = marked(markdownContent, {async: false})
return {
meta: metadata,
content: htmlContent
data: metadata,
html: htmlContent
}
}
}
const renderer = new marked.Renderer()

50
src/page.ts Normal file
View file

@ -0,0 +1,50 @@
import Renderer , {File} from "./renderer.js"
import path from 'path'
import utils from './utils.js'
export default class Page {
file: File
/// /example/path
dir: string[]
/// /example/path/file
path: string
/// /example/path/file.html
finalPath: string
/// file.html
fileName: string
hidden: boolean
title: string
layout: string
constructor(filePath: string, renderer: Renderer) {
this.file = renderer.loadAsHTML(filePath)
if (this.file.data?.path != null && this.file.data?.path != undefined) {
this.path = path.join("/", this.file.data?.path)
} else {
const parsedPath = path.parse(filePath)
const basePath = path.join("/", parsedPath.dir, parsedPath.name)
this.path = basePath
}
const parsedPath = path.parse(this.path)
parsedPath.ext = '.html'
parsedPath.base = `${parsedPath.name}${parsedPath.ext}`
this.finalPath = path.format(parsedPath)
const dirArray = utils.pathToArray(this.finalPath)
this.fileName = dirArray.pop() || ""
dirArray.shift()
this.dir = dirArray
this.hidden = this.file.data?.hidden || false
// TODO: if tilte do not exists search in markdown for # title
this.title = this.file.data.title
this.layout = this.file.data.layout || "default"
}
}

View file

@ -1,41 +1,83 @@
import { fileURLToPath } from 'url'
import path from 'path'
import { cp } from './utils.js'
import { build } from './site.js'
import ProjectConfig from './project-config.js'
import ProjectConfig from './config.js'
import fs from 'fs'
import pug from 'pug'
import utils from "./utils.js"
import Markdown from './markdown.js'
import Yaml from './yaml.js'
import Page from './page.js'
export default class Project {
public config = new ProjectConfig() // should be injected
private __filename: string
private __dirname: string
private DEFAULT_PROJECT_PATH: string
private config = new ProjectConfig()
pages: Page[]
constructor() {
// Get the directory of the current file
this.__filename = fileURLToPath(import.meta.url)
this.__dirname = path.dirname(this.__filename)
// Path relative to the script file's directory
this.DEFAULT_PROJECT_PATH = path.join(this.__dirname, '..', 'empty')
this.pages = this.loadPages()
console.log(this.pages.map(it=> {return {dir: it.dir, file: it.finalPath}}))
}
new() {
console.log("Initialize a new project")
console.log(this.DEFAULT_PROJECT_PATH)
cp(this.DEFAULT_PROJECT_PATH, ".")
console.log(this.config)
this.config.load()
}
existing() {
this.config.load()
cp(this.config.config.defaultProjectPath, ".")
}
build() {
this.config.load()
build(this.config.config)
utils.removeDirectorySync(this.config.config.buildDir)
utils.cp("./.sajt/static", path.join(this.config.config.buildDir, "static"))
for(const page of this.pages) {
this.compile(page)
}
}
loadPages(): Page[] {
const mdParser = new Markdown()
const yamlParser = new Yaml()
let listMD = utils.getAllFilesWithExtension('.',".md", this.config.config.ignore)
.map(path => new Page(path, mdParser))
let listYML = utils.getAllFilesWithExtension('.',".yml", this.config.config.ignore)
.map(path => new Page(path, yamlParser))
return listMD.concat(listYML)
}
compile(page: Page) {
const output = path.join(this.config.config.buildDir, page.finalPath)
const compiledFunction = pug.compileFile(`.sajt/layouts/${page.layout}.pug`)
const data = {
page,
context: this.context(page)
}
const dirname = path.dirname(output)
if (!fs.existsSync(dirname)) {
fs.mkdirSync(dirname, { recursive: true })
}
const html = compiledFunction(data)
fs.writeFileSync(output, html)
console.log(`> ${output}`)
}
context(page: Page): any {
return {
pages: this.pages.map(it => { return {
title: it.title,
isCurrent: page == it,
url: it.finalPath,
isDir: false
}})
}
}
}

10
src/renderer.ts Normal file
View file

@ -0,0 +1,10 @@
export interface File {
get html(): string
get data(): { [key: string]: any }
}
export default interface Renderer {
get name(): string
loadAsHTML(path: string): File
}

View file

@ -1,15 +1,11 @@
import express from 'express'
import { setWatcher } from './watch.js'
//import { webdavMiddleware } from "./webdav.js"
const app = express()
const PORT = process.env.PORT || 3000
export function serve() {
app.use(express.static('./.build'))
//app.use(webdavMiddleware)
setWatcher(url => {
console.log(url)
})

View file

@ -1,132 +0,0 @@
import fs from 'fs'
import pug from 'pug'
import path from 'path'
import { cp } from "./utils.js"
import { parseMarkdown } from './markdown.js'
import { getAllFilesWithExtension, pathToArray, parseYML } from './utils.js'
import { parseMD } from './markdown.js'
function removeDirectorySync(directory: string) {
try {
fs.rmSync(directory, { recursive: true, force: true })
console.log("Directory and its contents removed.")
} catch (err: any) {
console.error(`Error removing directory: ${err.message}`)
}
}
function compile(template: string, content: any, output: string) {
if (template == null) {
console.error("Template is not defined, loading the default")
template = "default"
}
const compiledFunction = pug.compileFile(`.sajt/layouts/${template}.pug`);
const data = {
...content,
site: {posts: []}
}
const dirname = path.dirname(output)
if (!fs.existsSync(dirname)) {
fs.mkdirSync(dirname, { recursive: true })
}
const html = compiledFunction(data)
fs.writeFileSync(output, html)
console.log(`HTML has been rendered and saved to ${output}`);
}
function compileData(template: string, content: object, output: string) {
const compiledFunction = pug.compileFile(`.sajt/layouts/${template}.pug`)
const dirname = path.dirname(output)
if (!fs.existsSync(dirname)) {
fs.mkdirSync(dirname, { recursive: true })
}
const html = compiledFunction(content)
fs.writeFileSync(output, html)
console.log(`HTML has been rendered and saved to ${output}`);
}
function readMetadata(ignore: string[]) {
let htmlExtension = "html"
let listMD = getAllFilesWithExtension('.',".md", ignore)
.map(f => { return {
pathMD: f,
type: "md",
data: {} as any,
md: parseMD(f)
} as any })
let listYML = getAllFilesWithExtension('.',".yml", ignore)
.map(f => { return {
pathMD: f,
type: "yml",
data: parseYML(f),
md: {meta: {}}
} as any })
let list = listMD.concat(listYML)
for(const site of list) {
if (site.md.meta?.path != null && site.md.meta?.path != undefined) {
site.path = path.join("/", site.md.meta.path)
} else {
const parsedPath = path.parse(site.pathMD)
const basePath = path.join("/", parsedPath.dir, parsedPath.name)
site.path = basePath
}
// add proper extension
const parsedPath = path.parse(site.path)
parsedPath.ext = htmlExtension.startsWith('.') ? htmlExtension : `.${htmlExtension}`
parsedPath.base = `${parsedPath.name}${parsedPath.ext}`
site.path = path.format(parsedPath)
// add dirs metadata
const dirArray = pathToArray(site.path)
site.fileName = dirArray.pop()
dirArray.shift()
site.dir = dirArray
site.meta = site.md.meta
site.hidden = site.data?.hidden || false
}
return list
}
export function build(config: any) {
removeDirectorySync(config.buildDir)
cp("./.sajt/static", path.join(config.buildDir, "static"))
let data = readMetadata(config.ignore)
let pages = data.map(site => {
return {
title: site.meta.title,
url: site.path
}
})
for(const site of data) {
if (site.type == "md") {
compile(site.meta.layout,
{
content: site.md.content,
title: site.meta.title,
hidden: false,
pages
},
path.join(config.buildDir, site.path))
} else if (site.type == "yml") {
let data = {...site.data}
delete data.layout
parseMarkdown(data)
compileData(site.data.layout,
{data, pages, hidden: data.hidden},
path.join(config.buildDir, site.path))
}
}
}

View file

@ -1,36 +0,0 @@
/*const Client = require('ssh2-sftp-client')
async function uploadDirectory(serverConfig, localDirPath) {
const sftp = new Client()
await sftp.connect(serverConfig)
try {
await upload(sftp, config, localDirPath, serverConfig.path)
} catch (err) {
console.error(`Error: ${err.message}`)
} finally {
await sftp.end()
console.log('Connection closed')
}
}
async function upload(sftp, config, localPath, remotePath) {
console.log('Connected to the server')
const files = fs.readdirSync(localPath)
for (const file of files) {
const localFilePath = path.join(localPath, file)
const remoteFilePath = `${remotePath}/${file}`
if (fs.statSync(localFilePath).isDirectory()) {
await sftp.mkdir(remoteFilePath, true)
await upload(sftp, config, localFilePath, remoteFilePath)
} else {
const fileContent = fs.readFileSync(localFilePath)
await sftp.put(Buffer.from(fileContent), remoteFilePath)
console.log(`File transferred successfully: ${localFilePath}`)
}
}
}
*/

View file

@ -1,12 +1,7 @@
import yaml from 'js-yaml'
import fs from 'fs'
import path from 'path'
export function parseYML(file: string) {
const fileContents = fs.readFileSync(file, 'utf8')
return yaml.load(fileContents)
}
export function getAllFilesWithExtension(directory: string, extension: string, excludes: string[]): string[] {
let results: string[] = []
function readDirectory(directory: string) {
@ -29,7 +24,6 @@ export function getAllFilesWithExtension(directory: string, extension: string, e
return results
}
// copyDirectory
export function cp(source: string, destination: string) {
fs.mkdirSync(destination, { recursive: true })
const items = fs.readdirSync(source)
@ -45,9 +39,23 @@ export function cp(source: string, destination: string) {
})
}
export function pathToArray(filePath: string) {
// Normalize the file path to handle different OS path separators
function removeDirectorySync(directory: string) {
try {
fs.rmSync(directory, { recursive: true, force: true })
console.log("Directory and its contents removed.")
} catch (err: any) {
console.error(`Error removing directory: ${err.message}`)
}
}
export function pathToArray(filePath: string):string[] {
const normalizedPath = path.normalize(filePath)
// Split the path into an array of directories
return normalizedPath.split(path.sep)
}
export default {
pathToArray,
cp,
getAllFilesWithExtension,
removeDirectorySync
}

View file

@ -2,34 +2,31 @@ import fs from 'fs'
import path from 'path'
export function setWatcher(callback: (url: string) => void) {
// Directory to watch
const directoryPath = './';
// Ensure the directory exists
const directoryPath = './'
if (!fs.existsSync(directoryPath)) {
fs.mkdirSync(directoryPath, { recursive: true });
fs.mkdirSync(directoryPath, { recursive: true })
}
console.log(`Watching for changes in: ${directoryPath}`);
// Watch the directory for changes
console.log(`Watching for changes in: ${directoryPath}`)
fs.watch(directoryPath, (eventType, filename) => {
if (filename) {
const fullPath = path.join(directoryPath, filename);
console.log(`File ${filename} has been ${eventType}`);
// Check if the file was added, changed, or deleted
const fullPath = path.join(directoryPath, filename)
console.log(`File ${filename} has been ${eventType}`)
fs.stat(fullPath, (err, stats) => {
if (err) {
if (err.code === 'ENOENT') {
console.log(`File ${filename} was deleted.`);
console.log(`File ${filename} was deleted.`)
} else {
console.error(`Error checking file status: ${err.message}`);
console.error(`Error checking file status: ${err.message}`)
}
} else {
if (stats.isFile()) {
console.log(`File ${filename} exists with size: ${stats.size} bytes`);
console.log(`File ${filename} exists with size: ${stats.size} bytes`)
}
}
});
})
} else {
console.log('Filename not provided, but change detected.');
console.log('Filename not provided, but change detected.')
}
});
})
}

View file

@ -1,31 +0,0 @@
/*
import { v2 as webdav } from 'webdav-server';
// 1. Create a user manager and add a user
const userManager = new webdav.SimpleUserManager()
const user = userManager.addUser('user', 'password', false)
// 2. Create a privilege manager
const privilegeManager = new webdav.SimplePathPrivilegeManager()
// 3. Configure the WebDAV server
const server = new webdav.WebDAVServer({
// HTTP Digest authentication for better security
httpAuthentication: new webdav.HTTPDigestAuthentication(userManager, 'default-realm'),
privilegeManager
})
// 4. Set up a physical file system folder (e.g., `./data`) as the root
const publicFileSystem = new webdav.PhysicalFileSystem('./')
server.setFileSystem('/', publicFileSystem, (success) => {
if (!success) {
console.error('Failed to set file system')
process.exit(1)
}
// Give the user all permissions on the root
privilegeManager.setRights(user, '/', ['all'])
});
export const webdavMiddleware = webdav.extensions.express("/", server)
*/

20
src/yaml.ts Normal file
View file

@ -0,0 +1,20 @@
import yaml from 'js-yaml'
import Renderer , {File} from "./renderer.js"
import fs from 'fs'
export default class Yaml implements Renderer {
name = "yml"
loadAsHTML(file: string): File {
const fileContents = fs.readFileSync(file, 'utf8')
// let data = {...site.data}
// delete data.layout
// parseMarkdown(data)
return {
data: yaml.load(fileContents) as { [key: string]: any },
html: ""
}
}
}