mirror of
https://github.com/RubyMetric/chsrc
synced 2025-07-16 04:07:27 +08:00
299 lines
8.6 KiB
Raku
299 lines
8.6 KiB
Raku
# ---------------------------------------------------------------
|
||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||
# ---------------------------------------------------------------
|
||
# 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-14>
|
||
#
|
||
# Generates C code from raw string
|
||
# ---------------------------------------------------------------
|
||
|
||
unit module Generator;
|
||
|
||
use Parser;
|
||
use Version;
|
||
|
||
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();
|
||
}
|
||
|
||
# 优先从 section-config 获取配置,如果没有则从 global-config 获取
|
||
my $prefix = $section-config.exist('prefix') ??
|
||
$section-config.get('prefix').as-string() !!
|
||
$global-config.get('prefix', '_rawstr4c').as-string();
|
||
|
||
my $language = $section-config.get('language').as-string();
|
||
my $postfix = self.resolve-postfix($global-config, $section-config, $language);
|
||
|
||
my $keep-prefix = $section-config.exist('keep-prefix') ??
|
||
$section-config.get('keep-prefix').as-bool() !!
|
||
$global-config.get('keep-prefix', 'true').as-bool();
|
||
|
||
my $keep-postfix = $section-config.exist('keep-postfix') ??
|
||
$section-config.get('keep-postfix').as-bool() !!
|
||
$global-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, $section-config, $language) {
|
||
# 优先从 section-config 获取 postfix
|
||
my $postfix = $section-config.exist('postfix') ??
|
||
$section-config.get('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 v{Version::VERSION}-{Version::RELEASE_DATE}
|
||
*
|
||
* 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\n";
|
||
} else {
|
||
$header ~= "extern char {$var<name>}[];\n";
|
||
}
|
||
}
|
||
when 'macro' {
|
||
$header ~= "#define {$var<name>.uc} \"{$var<content>}\"\n\n";
|
||
}
|
||
}
|
||
}
|
||
|
||
return $header;
|
||
}
|
||
|
||
method generate-c-source-file() {
|
||
my $source = qq:to/EOF/;
|
||
/**
|
||
* Generated by rawstr4c v{Version::VERSION}-{Version::RELEASE_DATE}
|
||
*
|
||
* Date: {DateTime.now.Str}
|
||
*/
|
||
|
||
#include "{$.c-header-filename}"
|
||
|
||
EOF
|
||
|
||
for @.variables -> $var {
|
||
if $var<type> eq 'global-variable' {
|
||
$source ~= "char {$var<name>}[] = \"{$var<content>}\";\n";
|
||
}
|
||
}
|
||
|
||
return $source;
|
||
}
|
||
|
||
|
||
method save-files($output-mode, $dest-dir) {
|
||
|
||
my $c-header-file = $dest-dir.IO.child($.c-header-filename).Str;
|
||
|
||
$c-header-file.IO.spurt(self.generate-c-header-file($output-mode));
|
||
say "Generated C header file: $c-header-file";
|
||
|
||
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');
|
||
my $c-source-file = $dest-dir.IO.child($c-source-filename).Str;
|
||
$c-source-file.IO.spurt(self.generate-c-source-file());
|
||
say "Generated C source file: $c-source-file";
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
class Generator {
|
||
has $.parser;
|
||
has $.cstring-converter;
|
||
has $.varname-generator;
|
||
has $.variable-generator;
|
||
|
||
method new($parser) {
|
||
self.bless(
|
||
:$parser,
|
||
:cstring-converter(CStringConverter.new),
|
||
:varname-generator(CVariableNameGenerator.new),
|
||
:variable-generator(CVariableGenerator.new)
|
||
);
|
||
}
|
||
|
||
method get-config-value($global-config, $section-config, $key, $default = '') {
|
||
# 优先级:section-config > global-config > default
|
||
if $section-config && $section-config.exist($key) {
|
||
return $section-config.get($key);
|
||
}
|
||
elsif $global-config && $global-config.exist($key) {
|
||
return $global-config.get($key);
|
||
}
|
||
else {
|
||
return $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;
|
||
|
||
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 "--- Section: $title ---";
|
||
say "Variable name = $var-name";
|
||
say "Translation mode = $translate-mode";
|
||
say "Output mode = $output-mode";
|
||
|
||
my $language = $section-config.get('language', 'None').as-string();
|
||
my $prefix = self.get-config-value($global-config, $section-config, 'prefix', '_rawstr4c').as-string();
|
||
say "Language = $language";
|
||
say "Prefix = $prefix (from " ~ ($section-config.exist('prefix') ?? 'section' !! 'global') ~ ")";
|
||
say '';
|
||
}
|
||
|
||
my $c-string = $.cstring-converter.convert-string($code, $translate-mode);
|
||
|
||
given $output-mode {
|
||
when 'terminal' {
|
||
say 'char ' ~ $var-name ~ '[] = "' ~ $c-string ~ '";';
|
||
say "";
|
||
}
|
||
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";
|
||
}
|
||
}
|
||
}
|
||
|
||
|
||
method generate() {
|
||
|
||
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' {
|
||
my $dest-dir = $.parser.input-file.dirname.Str;
|
||
$.variable-generator.save-files($output-mode, $dest-dir);
|
||
}
|
||
}
|
||
}
|