feat: Use Typescript, YAML, Handlebars with generation workflow

Took 5 hours 7 minutes
This commit is contained in:
Lucàs
2025-07-14 00:27:06 +02:00
parent 3318a6cede
commit 48bde33a0e
40 changed files with 604 additions and 428 deletions
+12
View File
@@ -0,0 +1,12 @@
/**
* Compiler interface for compiling templates with data.
*/
export interface Compiler {
/**
* Compiles the given template with the provided data.
* @param template - The template string to compile.
* @param data - The data object to use for compilation.
*/
compile(template: string, data: object): string;
}
+22
View File
@@ -0,0 +1,22 @@
import {type Compiler, CompilerType, HandlebarsCompiler} from '.';
/**
* Factory class for creating compiler instances.
* This class provides a method to get a compiler based on the specified type.
*/
export class CompilerFactory {
/**
* Creates a compiler instance based on the specified type.
* @param type - The type of compiler to create.
* @returns An instance of the specified compiler.
*/
public static getCompiler(type: CompilerType): Compiler {
switch (type) {
case CompilerType.HANDLEBARS:
return HandlebarsCompiler.getInstance();
default:
throw new Error(`Unsupported compiler type: ${type}`);
}
}
}
+6
View File
@@ -0,0 +1,6 @@
/**
* Enum representing different types of compilers.
*/
export enum CompilerType {
HANDLEBARS = 'handlebars',
}
+25
View File
@@ -0,0 +1,25 @@
import {type Compiler, CompilerType} from '.';
import {compile} from 'handlebars';
export class HandlebarsCompiler implements Compiler {
static readonly TYPE: CompilerType = CompilerType.HANDLEBARS;
private static instance: HandlebarsCompiler;
private constructor() {}
public static getInstance(): HandlebarsCompiler {
if (!HandlebarsCompiler.instance) {
HandlebarsCompiler.instance = new HandlebarsCompiler();
}
return HandlebarsCompiler.instance;
}
/**
* @inheritdoc
*/
public compile(template: string, data: object = {}): string {
const compiledTemplate = compile(template);
return compiledTemplate(data);
}
}
+4
View File
@@ -0,0 +1,4 @@
export * from './Compiler';
export * from './CompilerType';
export * from './CompilerFactory';
export * from './HandlebarsCompiler';
-31
View File
@@ -1,31 +0,0 @@
import yaml
from src.model.skill_list import skill_list
import requests
from src.model.social_list import social_list
class Config:
config_file_path: str
config_data: dict[str, any] = None
def __init__(self, config_file_path: str):
self.config_file_path = config_file_path
def load_config_file(self):
with open(self.config_file_path, 'r') as config_file:
self.config_data = yaml.safe_load(config_file)
def handle_user_info(self):
user = self.config_data["user"]
response = requests.get(f"https://api.github.com/users/{user}")
if response.status_code != 200:
raise Exception("User not found")
self.config_data["user"] = response.json()
def get_data(self):
self.load_config_file()
self.handle_user_info()
self.config_data["skills"] = skill_list(self.config_data["skills"])
self.config_data["socials"] = social_list(self.config_data["socials"])
return self.config_data
+75
View File
@@ -0,0 +1,75 @@
import config from '../config.json';
import {CompilerFactory, CompilerType} from './compilers';
import {ParserFactory, ParserType} from './parsers';
import {Validator} from './validators/Validator.ts';
import {FileManager} from './io/FileManager.ts';
import {ReadmeSchema} from './validators/schemas';
/**
* Point d'entrée du script
*/
async function main(): Promise<void> {
const compiler = createCompiler(config.compiler_type);
const parser = createParser(config.parser_type);
const validator = new Validator(ReadmeSchema);
const data = await loadData(config.data_file, parser, validator);
const template = await loadTemplate(config.input_file);
const result = compiler.compile(template, data);
await saveOutput(config.output_file, result);
}
/**
* Crée un compilateur en fonction du type spécifié dans la configuration
* @param type - Le type de compilateur à créer
*/
function createCompiler(type: string) {
const compilerType = CompilerType[type as keyof typeof CompilerType];
return CompilerFactory.getCompiler(compilerType);
}
/**
* Crée un parseur en fonction du type spécifié dans la configuration
* @param type - Le type de parseur à créer
*/
function createParser(type: string) {
const parserType = ParserType[type as keyof typeof ParserType];
return ParserFactory.getParser(parserType);
}
/**
* Charge les données depuis le fichier spécifié, les parse et les valide
* @param path - Le chemin du fichier de données
* @param parser - Le parseur à utiliser pour analyser les données
* @param validator - Le validateur à utiliser pour valider les données
*/
async function loadData(
path: string, parser: ReturnType<typeof createParser>,
validator: Validator) {
const raw = await FileManager.read(path);
const parsed = parser.parse(raw);
return validator.validate(parsed);
}
/**
* Charge le modèle de template depuis le fichier spécifié
* @param path - Le chemin du fichier de template
*/
async function loadTemplate(path: string): Promise<string> {
return FileManager.read(path);
}
/**
* Enregistre le résultat compilé dans le fichier de sortie spécifié
* @param path - Le chemin du fichier de sortie
* @param content - Le contenu à écrire dans le fichier
*/
async function saveOutput(path: string, content: string): Promise<void> {
await FileManager.write(path, content);
}
// Exécute le script principal
main()
.then(() => console.log('🎉 Exécution terminée avec succès.'))
.catch(err => console.error('❌ Erreur pendant lexécution:', err));
+33
View File
@@ -0,0 +1,33 @@
import {readFileSync, writeFileSync} from 'node:fs';
/**
* FileManager is a singleton class that provides methods to read and write files.
* It ensures that only one instance of the FileManager exists throughout the application.
*/
export class FileManager {
/**
* Reads the content of a file at the specified path.
* @param {string} filePath - The path to the file to read.
* @returns {Promise<string>} A promise that resolves with the file content.
*/
public static async read(filePath: string): Promise<string> {
return readFileSync(filePath, {
encoding: 'utf-8',
});
}
/**
* Writes content to a file at the specified path.
* If the file does not exist, it will be created.
* @param {string} filePath - The path to the file to write.
* @param {string} content - The content to write to the file.
* @returns {Promise<void>} A promise that resolves when the write operation is complete.
*/
public static async write(filePath: string, content: string): Promise<void> {
return writeFileSync(filePath, content, {
encoding: 'utf-8',
flag: 'w',
});
}
}
-13
View File
@@ -1,13 +0,0 @@
from src.shield.skill_shield import SkillShield
class Skill:
alt: str
src: str
def __init__(self, name: str):
self.alt = name
self.src = SkillShield(name).__str__()
def __str__(self) -> str:
return f"![{self.alt}]({self.src})"
-11
View File
@@ -1,11 +0,0 @@
from src.model.skill import Skill
def skill_list(skills: list[str]) -> dict[str, str]:
# Sort and remove duplicates
skills = list(set(skills))
skills.sort()
skills = {skill: Skill(skill).__str__() for skill in skills}
skills["all"] = "\n".join([str(skill) for skill in skills.values()])
return skills
-14
View File
@@ -1,14 +0,0 @@
from src.shield.social_shield import SocialShield
class Social:
name: str
img: str
def __init__(self, name: str, url: str):
self.name = name
self.img = SocialShield(name, url).__str__()
self.url = url
def __str__(self) -> str:
return f"[![{self.name}]({self.img})]({self.url})"
-8
View File
@@ -1,8 +0,0 @@
from src.model.social import Social
def social_list(socials: list) -> dict[str, str]:
socials: dict[str, str] = {social.get("name"): str(Social(social.get("name"), social.get("url"))) for social in
socials}
socials["all"] = "\n".join([str(social) for social in socials.values()])
return socials
+17
View File
@@ -0,0 +1,17 @@
/**
* Parser interface for parsing data strings into object representations.
*/
export interface Parser {
/**
* Parses the given data string and returns an object representation.
* @param data - The data string to parse.
* @returns An object representation of the parsed data.
*/
parse(data: string): object;
/**
* Returns an instance of the parser.
* @returns An instance of the parser.
*/
getInstance(): Parser;
}
+20
View File
@@ -0,0 +1,20 @@
import {ParserType, type Parser, YamlParser} from "."
/**
* Factory class for creating parser instances.
*/
export class ParserFactory {
/**
* Creates and returns a parser instance based on the specified type.
* @param type
*/
static getParser(type: ParserType): Parser {
switch (type) {
case ParserType.YAML:
return YamlParser.getInstance();
default:
throw new Error(`Unsupported parser type: ${type}`);
}
}
}
+6
View File
@@ -0,0 +1,6 @@
/**
* Enum representing different parser types.
*/
export enum ParserType {
YAML = 'yaml',
}
+49
View File
@@ -0,0 +1,49 @@
import {type Parser, ParserType} from '.';
import {parse} from 'yaml';
/**
* YamlParser class implements the Parser interface for parsing YAML data.
*/
export class YamlParser implements Parser {
/**
* The type of the parser.
* This is used to identify the parser type in the system.
*/
static readonly TYPE: ParserType = ParserType.YAML;
/**
* Singleton instance of the parser.
* This ensures that only one instance of the parser exists.
*/
static instance?: YamlParser;
private constructor() {
}
/**
* @inheritdoc
*/
public static getInstance(): YamlParser {
if (!this.instance) {
this.instance = new YamlParser();
}
return this.instance;
}
getInstance(): Parser {
throw new Error('Method not implemented.');
}
/**
* @inheritdoc
*/
public parse(data: string): object {
try {
return parse(data);
} catch (error) {
console.error('Error parsing YAML data:', error);
throw new Error('Failed to parse YAML data');
}
}
}
+4
View File
@@ -0,0 +1,4 @@
export * from "./Parser";
export * from "./ParserType";
export * from "./ParserFactory";
export * from "./YamlParser";
-94
View File
@@ -1,94 +0,0 @@
from urllib.parse import urlunsplit, urlencode
from simpleicons.all import icons
class ShieldBuilder:
BASE_URL = "https://img.shields.io/static/v1"
message: str = None
style: str = None
logo: str = None
logo_color: str = None
label: str = None
label_color: str = None
color: str = None
cache_seconds: int = None
link: str = None
def __init__(self):
self.logo_color = "white"
self.label = " "
self.color = "black"
def set_message(self, message: str):
self.message = (
message
.replace("_", "__")
.replace("-", "--")
.replace(" ", "_")
)
return self
def set_style(self, style: str):
if not style in ["flat", "flat-square", "plastic", "for-the-badge", "social"]:
raise Exception("Invalid style")
self.style = style
return self
def set_logo(self, logo: str):
self.logo = icons.get(logo).slug if logo in icons else None
return self
def set_logo_color(self, logo_color: str):
self.logo_color = logo_color
return self
def set_label(self, label: str):
self.label = label
return self
def set_label_color(self, label_color: str):
self.label_color = label_color
return self
def set_color(self, color: str):
self.color = color
return self
def set_cache_seconds(self, cache_seconds: int):
self.cache_seconds = cache_seconds
return self
def set_link(self, link: str):
self.link = link
return self
def get_query(self):
query = {
"message": self.message,
"style": self.style,
"logo": self.logo,
"logoColor": self.logo_color,
"label": self.label,
"labelColor": self.label_color,
"color": self.color,
"cacheSeconds": self.cache_seconds,
"link": self.link
}
# Remove None values
return {k: v for k, v in query.items() if v is not None}
def build(self):
query = urlencode(self.get_query())
return urlunsplit(("", "", self.BASE_URL, query, ""))
if "__main__" == __name__:
shield_builder = (
ShieldBuilder()
.set_logo("HTML5")
.set_message("HTML5")
.build()
)
print(shield_builder)
-33
View File
@@ -1,33 +0,0 @@
from simpleicons.all import icons
from src.shield.shield_builder import ShieldBuilder
class SkillShield:
def __init__(self, name: str = None):
self.builder = ShieldBuilder()
self.skill = self.set_skill(name) if name is not None else None
def get_skill(self):
return self.skill
def set_skill(self, name: str):
self.skill = icons.get(name)
if self.skill is not None:
(self.builder.set_message(self.skill.title)
.set_logo(self.skill.slug)
.set_color(self.skill.hex)
)
else:
self.builder.set_message(name)
def get_builder(self):
return self.builder
def __repr__(self):
self.__str__()
def __str__(self):
return self.builder.build()
-37
View File
@@ -1,37 +0,0 @@
from simpleicons.all import icons
from src.shield.shield_builder import ShieldBuilder
class SocialShield:
def __init__(self, name: str = None, url: str = None):
self.builder = ShieldBuilder().set_style("for-the-badge")
self.name = name
self.url = url
self.social = self.set_social(name, url) if name is not None and url is not None else None
def get_social(self):
return self.social
def set_social(self, name: str, url: str):
self.social = icons.get(name)
if self.social is not None:
(self.builder.set_message(self.social.title)
.set_logo(self.social.slug)
.set_color(self.social.hex)
)
else:
self.builder.set_message(name)
self.builder.set_link(url)
def get_builder(self):
return self.builder
def __repr__(self):
self.__str__()
def __str__(self):
return self.builder.build()
-10
View File
@@ -1,10 +0,0 @@
class Template:
def __init__(self, template_path: str):
self.template_path = template_path
def render(self, **kwargs):
with open(self.template_path, 'r') as f:
template = f.read()
return template.format(**kwargs)
+37
View File
@@ -0,0 +1,37 @@
## Hi there! 👋
I'm **{{author.first_name}}**, a passionate developer based in **{{author.location}}**.
### 🚀 What I do
- 💻 Currently, I'm studying at **{{author.company}}** for my Master's degree in Informatics.
- 🌐 I specialize in Web Development, and I'm always eager to explore new technologies and frameworks.
- 🌱 I'm constantly learning and expanding my skill set to stay up-to-date with the ever-evolving tech landscape.
### 🌍 Connect with me
{{#each socials as | social |}}
[![{{social.name}}]({{social.icon}})]({{social.url}})
{{/each}}
### 🛠️ Tech Stack
{{#each skills as | skill |}}
![{{skill.name}}]({{skill.icon}})
{{/each}}
### 🤝 Let's collaborate
👀 I'm always open to collaboration and exciting projects. If you have something in mind, feel free to reach out!
---
<footer><div align="center">
![SVG Stats](https://github-stats-alpha.vercel.app/api?username=LucasVbr&cc=000&tc=fff&ic=fff&bc=000)
![Profile Views](https://komarev.com/ghpvc/?username=lucasvbr&amp;amp;amp;label=Profile%20views&amp;amp;amp;color=0e75b6&amp;amp;amp;style=flat)
![FreeCodeCamp Points](https://img.shields.io/freecodecamp/points/lucasvbr?label=FreeCodeCamp%20points)
![Made with love](https://img.shields.io/badge/-made%20with%20%E2%9D%A4%EF%B8%8F-red)
</div></footer>
+43
View File
@@ -0,0 +1,43 @@
import type {ZodObject} from 'zod';
/**
* Validator class for validating and parsing objects against a Zod schema.
*/
export class Validator {
/**
* Creates an instance of the Validator.
* @param schema - The Zod schema to validate against.
*/
constructor(private schema: ZodObject<any, any>) {
}
/**
* Validates the given value against the schema.
* @param value - The object to validate.
* @returns true if the value is valid, false otherwise.
*/
public isValid(value: object): boolean {
try {
this.schema.parse(value);
return true;
} catch (error) {
console.error('Validation error:', error);
return false;
}
}
/**
* Parses the given value against the schema.
* @param value - The object to parse.
* @returns The parsed object if valid, throws an error otherwise.
*/
public validate(value: object): object {
try {
return this.schema.parse(value);
} catch (error) {
console.error('Parsing error:', error);
throw new Error('Failed to parse value');
}
}
}
+7
View File
@@ -0,0 +1,7 @@
import {z} from 'zod';
export const AuthorSchema = z.object({
first_name: z.string(),
location: z.string(),
company: z.string(),
});
+8
View File
@@ -0,0 +1,8 @@
import {z} from 'zod';
import {AuthorSchema, SkillSchema, SocialSchema} from '.';
export const ReadmeSchema = z.object({
author: AuthorSchema,
socials: z.array(SocialSchema),
skills: z.array(SkillSchema),
});
+6
View File
@@ -0,0 +1,6 @@
import {z} from 'zod';
export const SkillSchema = z.object({
name: z.string(),
icon: z.string(),
});
+7
View File
@@ -0,0 +1,7 @@
import {z} from 'zod';
export const SocialSchema = z.object({
name: z.string(),
icon: z.string(),
url: z.string(),
});
+4
View File
@@ -0,0 +1,4 @@
export * from './AuthorSchema';
export * from './SocialSchema';
export * from './SkillSchema';
export * from './ReadmeSchema';