Move to tool

This commit is contained in:
Aoran Zeng
2025-07-13 19:53:19 +08:00
parent e1a32fe044
commit 7d7ff19d86
10 changed files with 18 additions and 13 deletions

View File

@@ -0,0 +1,267 @@
#!/usr/bin/env raku
# ---------------------------------------------------------------
# File Name : Generator.rakumod
# File Authors : Aoran Zeng <ccmywish@qq.com>
# Contributors : Nul None <nul@none.org>
# Created On : <2025-07-12>
# Last Modified : <2025-07-13>
#
# Generates C code from raw string
# ---------------------------------------------------------------
unit module Generator;
use Parser;
my class CStringConverter {
method convert-char($char, $mode) {
given $mode {
when 'oct' {
my $bytes = $char.encode('UTF-8');
return $bytes.map({ "\\" ~ sprintf("%03o", $_) }).join('');
}
when 'hex' {
my $bytes = $char.encode('UTF-8');
return $bytes.map({ "\\x" ~ sprintf("%02x", $_) }).join('');
}
when 'escape' {
given $char {
when '"' { return '\\"'; }
when "'" { return "\\'"; }
when '\\' { return '\\\\'; }
when "\n" { return '\\n'; }
when "\t" { return '\\t'; }
when "\r" { return '\\r'; }
when "\0" { return '\\0'; }
default { return $char; }
}
}
default { die "Unknown translation mode: $mode"; }
}
}
method convert-string($content, $mode) {
my $result = "";
for $content.comb -> $char {
$result ~= self.convert-char($char, $mode);
}
return $result;
}
}
my class CVariableNameGenerator {
method generate($global-config, $section-config, $title) {
# 检查 name-literally
my $name-literally = $section-config.get('name-literally');
if $name-literally.is-bool() && $name-literally.as-bool() {
return $section-config.get('name', $title.lc).as-string();
}
my $prefix = $global-config.get('prefix', '_rawstr4c').as-string();
my $language = $section-config.get('language').as-string();
my $postfix = self.resolve-postfix($global-config, $language);
my $keep-prefix = $section-config.get('keep-prefix', 'true').as-bool();
my $keep-postfix = $section-config.get('keep-postfix', 'true').as-bool();
my $name = $section-config.get('name', $title.lc).as-string();
$name = $name.subst(/\s+/, '_', :g);
# 组装变量名
my $var-name = "";
$var-name ~= $prefix if $keep-prefix && $prefix;
$var-name ~= "_" if $var-name && $name;
$var-name ~= $name if $name;
$var-name ~= "_" if $var-name && $postfix && $keep-postfix;
$var-name ~= $postfix if $postfix && $keep-postfix;
return $var-name || "unnamed_var";
}
method resolve-postfix($global-config, $language) {
my $postfix = $global-config.get('postfix');
if $postfix.is-mode() && $postfix.as-mode() eq 'use-language' {
return $language ?? 'in_' ~ $language !! '';
}
# 如果不是模式,那就是用户给了一个具体的字符串
return $postfix.as-string();
}
}
#| .h / .c @variables
my class CVariableGenerator {
has @.variables;
has $.c-header-filename;
method new() {
my $c-header-filename = "rawstr4c.h";
self.bless(:$c-header-filename, :variables([]));
}
method add-variable($name, $content, $type) {
@.variables.push: {
name => $name,
content => $content,
type => $type
};
}
method generate-c-header-file($output-mode = 'global-variable') {
my $header = qq:to/EOF/;
#pragma once
/**
* Generated by rawstr4c
*
* Date: {DateTime.now.Str}
*/
EOF
for @.variables -> $var {
given $var<type> {
when 'global-variable' {
if $output-mode eq 'global-variable-only-header' {
$header ~= "char {$var<name>}[] = \"{$var<content>}\";\n";
} else {
$header ~= "extern char {$var<name>}[];\n";
}
}
when 'macro' {
$header ~= "#define {$var<name>.uc} \"{$var<content>}\"\n";
}
}
}
return $header;
}
method generate-c-source-file() {
my $source = qq:to/EOF/;
#include "{$.c-header-filename}"
/**
* Generated by rawstr4c
*
* Date: {DateTime.now.Str}
*/
EOF
for @.variables -> $var {
if $var<type> eq 'global-variable' {
$source ~= "char {$var<name>}[] = \"{$var<content>}\";\n";
}
}
return $source;
}
method save-files($output-mode) {
$.c-header-filename.IO.spurt(self.generate-c-header-file($output-mode));
say "Generated C header file: $.c-header-filename";
if $output-mode eq 'global-variable' {
my $has-globals = @.variables.grep({ $_<type> eq 'global-variable' }).elems > 0;
if $has-globals {
my $c-source-filename = $.c-header-filename.subst(/'.h'$/, '.c');
$c-source-filename.IO.spurt(self.generate-c-source-file());
say "Generated C source file: $c-source-filename";
}
}
}
}
class Generator {
has $.cstring-converter;
has $.varname-generator;
has $.variable-generator;
method new() {
self.bless(
:cstring-converter(CStringConverter.new),
:varname-generator(CVariableNameGenerator.new),
:variable-generator(CVariableGenerator.new)
);
}
method get-config-value($global-config, $section-config, $key, $default = '') {
return $section-config.exist($key) ??
$section-config.get($key) !!
$global-config.get($key, $default);
}
method generate-for-section($global-config, $section) {
my $section-config = $section<config>;
my $title = $section<title>;
my $code = $section<raw-string>;
my $debug-parser = $global-config.get('debug', False).as-bool();
return unless $code;
say "=== Section: $title ===";
my $translate-mode = self.get-config-value(
$global-config, $section-config, 'translate', ':escape'
).as-mode();
my $var-name = $.varname-generator.generate($global-config, $section-config, $title);
my $output-mode = $global-config.get('output', ':terminal').as-mode();
if $debug-parser {
say "Variable name: $var-name";
say "Translation mode: $translate-mode";
say "Output mode: $output-mode";
my $language = $section-config.get('language', 'None').as-string();
say "Language: $language";
say '';
}
my $c-string = $.cstring-converter.convert-string($code, $translate-mode);
given $output-mode {
when 'terminal' {
say 'char ' ~ $var-name ~ '[] = "' ~ $c-string ~ '";';
}
when 'global-variable' | 'global-variable-only-header' {
$.variable-generator.add-variable($var-name, $c-string, 'global-variable');
}
when 'macro' {
$.variable-generator.add-variable($var-name, $c-string, 'macro');
}
default {
die "Illegal output mode: $output-mode";
}
}
say "\n";
}
method generate($parser) {
my $global-config = $parser.global-config;
# 这个 generate-for-section() 要么把变量输出到终端,要么累计到 @variabels 中
for $parser.sections -> $section {
self.generate-for-section($global-config, $section);
}
my $output-mode = $global-config.get('output', ':terminal').as-mode();
# 最后把累计到 @variables 的内容输出到文件
if $output-mode ne 'terminal' {
$.variable-generator.save-files($output-mode);
}
}
}

View File

@@ -0,0 +1,246 @@
#!/usr/bin/env raku
# ---------------------------------------------------------------
# File Name : Parser.rakumod
# File Authors : Aoran Zeng <ccmywish@qq.com>
# Contributors : Nul None <nul@none.org>
# Created On : <2025-07-12>
# Last Modified : <2025-07-13>
#
# rawstr4c.md parsing
# ---------------------------------------------------------------
unit module Parser;
#| Bool Boolean
my enum ConfigValueType <String Mode Boolean>;
#|
my class ConfigValue {
has ConfigValueType $.type;
has $.raw-value;
has $.parsed-value;
method new($raw-text) {
my $type;
my $parsed;
given $raw-text {
when /^ ':' (.+) $/ {
# 模式值 :mode
$type = Mode;
$parsed = ~$0;
}
when /^ ('true'|'false'|'yes'|'no') $/ {
# 特殊字面量 - true/false/yes/no 都是 literal
$type = Boolean;
$parsed = ~$0 ~~ /^('true'|'yes')$/ ?? True !! False;
}
default {
# 普通字符串
$type = String;
$parsed = $raw-text;
}
}
self.bless(:$type, :raw-value($raw-text), :parsed-value($parsed));
}
method as-string() {
return $.parsed-value.Str;
}
method as-bool() {
given $.type {
when Boolean { return $.parsed-value; }
when String {
# 尝试将字符串解析为布尔值
return $.parsed-value ~~ /^('true'|'yes')$/;
}
default { return False; }
}
}
# 获取模式值(去掉冒号前缀)
method as-mode() {
return $.type == Mode ?? $.parsed-value !! $.raw-value;
}
# 类型检查方法
method is-mode() { return $.type == Mode; }
method is-bool() { return $.type == Boolean; }
method is-string() { return $.type == String; }
}
#| config items
my class Config {
has %.items;
# 如果非要在程序内部中调用,而不是直接从 Markdown 文件中读取出来
# 一定要记得 $raw-value 用的是 rawstr4c 的语法!也就是说,这里一定是一个字符串
method set($k, $raw-value) {
%.items{$k} = ConfigValue.new($raw-value);
}
method get($k, $default = Nil) {
return %.items{$k} // ($default ?? ConfigValue.new($default) !! ConfigValue.new(''));
}
method exist($k) {
return %.items{$k}:exists;
}
# 配置项名称
# @danger: 把这个函数命名为 items会让我的机器蓝屏.....
method keys() {
return %.items.keys;
}
}
#|( :
1. Global dom
2. Section dom
Global dom code block. code block Section dom
Parser :
- $global-config
- @sections ( $section)
$section Hash:
- title
- level
- raw-string
- config
)
class Parser {
has $.global-config;
has @.sections;
method new() {
self.bless(
global-config => Config.new(),
sections => []
);
}
# 配置项所在行 -> 解析为配置项
method parse-config-item-line($line, $section-config) {
# 语法: - key = `value`
if $line ~~ /^ '-' \s* (<[a..z\-]>+) \s* '=' \s* '`' (.+?) '`' / {
my $key = ~$0;
my $value = ~$1;
$section-config.set($key, $value);
return True;
}
return False;
}
method parse($content) {
my @lines = $content.lines;
my $current-section;
my $current-section-config = Config.new();
my $in-global = True;
my $in-code-block = False;
# 在代码块中的 raw string
my $rawstr = "";
# 开始遍历
for @lines -> $line {
# Step1: 记录层次 level
#
# @note 我们要避免,在代码块中也有 # 字符,比如在代码块里写的是 shell 脚本
if !$in-code-block && $line ~~ /^ '#' ('#'*) \s* (.+) / {
my $level = 1 + $0.chars;
my $title = ~$1;
# 只有匹配到下一个标题时,才说明前一个 section 已经结束,此时才有机会存下来
# Global dom 里是没有 raw string 的,所以被这里的条件排除掉了
if $rawstr && $current-section && $current-section<title> {
$current-section<raw-string> = $rawstr;
$current-section<config> = $current-section-config;
@.sections.push: $current-section;
}
$rawstr = "";
if $level == 1 {
$in-global = True;
$current-section = {};
$current-section-config = Config.new();
} else {
$in-global = False;
$current-section = {
title => $title,
level => $level,
};
$current-section-config = Config.new();
}
next;
}
# Step2: 处理配置项
if $in-global {
if self.parse-config-item-line($line, $.global-config) {
next;
}
} elsif $current-section {
if self.parse-config-item-line($line, $current-section-config) {
next;
}
}
# Step3: 开始处理raw string
if $line ~~ /^ '```' (.*)? / {
if $in-code-block {
$in-code-block = False;
} else {
$in-code-block = True;
my $lang = ~($0 // '');
if $lang && $current-section && !$current-section-config.exist('language') {
$current-section-config.set('language', $lang);
}
}
next;
}
# 代码块里的内容统统进来
if $in-code-block {
$rawstr ~= $line ~ "\n";
}
}
# 遍历结束, 这意味着文件已经阅读完毕最后一个section还没存现在存它
if $rawstr && $current-section && $current-section<title> {
$current-section<raw-string> = $rawstr;
$current-section<config> = $current-section-config;
@.sections.push: $current-section;
}
}
# 输出 config, 包含 global config 以及 section config
method debug() {
say "Global config:";
for $.global-config.keys.sort -> $item {
my $value = $.global-config.get($item);
say " $item = {$value.as-string} (type: {$value.type})";
}
say "";
# 设置debug标志后续在 Generator 中根据此信息输出 section config
$.global-config.set('debug', "true");
say "Found " ~ @.sections.elems ~ " sections:";
for @.sections -> $section {
say "Section: " ~ $section<title>;
}
say "";
}
}