Files
fltk/fluid/io/Code_Writer.cxx
T
Matthias Melcher 786bdbfd54 Fluid: remove three global state variables
* improves handling of class in class
  (still not recommended)
2026-08-13 18:10:45 +02:00

1091 lines
34 KiB
C++

//
// Fluid C++ Code Writer code for the Fast Light Tool Kit (FLTK).
//
// Copyright 1998-2026 by Bill Spitzak and others.
//
// This library is free software. Distribution and use rights are outlined in
// the file "COPYING" which should have been included with this file. If this
// file is missing or damaged, see the license at:
//
// https://www.fltk.org/COPYING.php
//
// Please see the following page on how to report bugs and issues:
//
// https://www.fltk.org/bugs.php
//
#include "io/Code_Writer.h"
#include "Fluid.h"
#include "Project.h"
#include "proj/mergeback.h"
#include "nodes/Window_Node.h"
#include "nodes/Function_Node.h"
#include <FL/filename.H>
#include "../../src/flstring.h"
#include <zlib.h>
#include <string>
#include <iomanip>
#include <iostream>
#include <sstream>
using namespace fluid;
using namespace fluid::io;
using namespace fluid::proj;
/**
Return a string with the given value formatted as an 8-digit hexadecimal number.
\param[in] value a 32-bit unsigned integer
\return string with the hexadecimal representation of the value
*/
std::string fluid::io::to_string_8x(uint32_t value) {
char buf[32];
snprintf(buf, sizeof(buf), "%08x", value);
return std::string(buf);
}
/**
Return a string with the given value formatted using the `%g` specifier.
\param[in] value a double-precision floating-point number
\return string with the formatted representation of the value
*/
std::string fluid::io::to_string_g(double value) {
char buf[32];
snprintf(buf, sizeof(buf), "%g", value);
return std::string(buf);
}
/**
Return true if c can be in a C identifier.
I needed this so it is not messed up by locale settings.
\param[in] c a character, or the start of a utf-8 sequence
\return 1 if c is alphanumeric or '_'
*/
int is_id(char c) {
return (c>='a' && c<='z') || (c>='A' && c<='Z') || (c>='0' && c<='9') || c=='_';
}
/** \brief Return a unique name for the given object within this file.
This is used to create unique identifiers for callback functions, menu items,
and other objects that need a unique name in file scope.
This function combines the name and label into an identifier. It then checks
if that id was already taken by another object, and if so, appends a
hexadecimal value which is incremented until the id is unique in this file.
If a new id was created, it is stored in the id tree.
\param[in] o create an ID for this object
\param[in] type is the first word of the ID
\param[in] name if name is set, it is appended to the ID
\param[in] label else if label is set, it is appended, skipping non-keyword characters
\return a unique identifier for this object and type
*/
std::string Code_Writer::unique_id(void* o, const std::string& type, const std::string& name, const std::string& label) {
char buffer[128];
char* q = buffer;
char* q_end = q + 128 - 8 - 1; // room for hex number and NUL
const char* type_cstr = type.c_str();
while (*type_cstr) *q++ = *type_cstr++;
*q++ = '_';
const char* n = name.c_str();
if (name.empty()) n = label.c_str();
if (n && *n) {
while (*n && !is_id(*n)) n++;
while (is_id(*n) && (q < q_end)) *q++ = *n++;
}
*q = 0;
// okay, search the tree and see if the name was already used:
int which = 0;
for (;;) {
auto it = unique_id_list.find(buffer);
// If the id does not exist, add it to the map
if (it == unique_id_list.end()) {
it = unique_id_list.insert(std::make_pair(buffer, o)).first;
return it->first;
}
// If it does exist, and the pointers are the same, just return it.
if (it->second == o) {
return it->first;
}
// Else repeat until we have a new id,
sprintf(q,"%x",++which);
}
}
/**
Return a C string that indents code to the given depth.
Indentation can be changed by modifying the multiplicator (``*2`` to keep
the FLTK indent style). Changing `spaces` to a list of tabs would generate
tab indents instead. This function can also be used for fixed depth indents
in the header file.
Do *not* ever make this a user preference, or you will end up writing a
fully featured code formatter.
\param[in] set generate this indent depth
\return indenting string
*/
std::string Code_Writer::indent(int set) const {
return std::string(set*2, ' ');
}
/**
Return a string that indents code to the current source file depth.
\return indenting string
*/
std::string Code_Writer::indent() const {
return indent(indentation);
}
/**
Return a string that indents code to the current source file depth plus an offset.
\param[in] offset adds a temporary offset for this call only; this does not
change the `indentation` variable; offset can be negative
\return indenting string
*/
std::string Code_Writer::indent_plus(int offset) const {
return indent(indentation+offset);
}
/**
Write a line of code to the header file unless the same line was written before.
\param[in] code line of code, a newline character will be appended
\return 1 if the text was added to the file, 0 if it was previously written.
*/
int Code_Writer::write_h_once(const std::string& code) {
if (text_in_header.find(code) != text_in_header.end()) {
return 0;
}
header_buffer << code << "\n";
text_in_header.insert(code);
return 1;
}
/**
Write multiple lines of code to the header file unless the same line was written before.
\param[in] code block of code, newline characters will be appended
*/
void Code_Writer::write_block_h_once(const std::string& code) {
std::istringstream iss(code);
std::string line;
while (std::getline(iss, line)) {
if (line.empty()) {
write_h("\n");
} else {
write_h_once(line);
}
}
}
/**
Write a line of code to the source file unless the same line was written before to the header or source file.
\param[in] code line of code, a newline character will be appended
\return 1 if the text was added to the file, 0 if it was previously written.
*/
int Code_Writer::write_c_once(const std::string& code) {
if (text_in_header.find(code) != text_in_header.end()) {
return 0;
}
if (text_in_code.find(code) != text_in_code.end()) {
return 0;
}
crc_puts(code);
crc_putc('\n');
text_in_code.insert(code);
return 1;
}
/**
Return true if this pointer was already included in the code file.
If it was not, add it to the list and return false.
\param[in] pp any pointer
\return true if found in the tree, false if added to the tree
*/
bool Code_Writer::c_contains(void *pp) {
if (ptr_in_code.find(pp) != ptr_in_code.end()) {
return true;
}
ptr_in_code.insert(pp);
return false;
}
/**
Write a C string to the code file, escaping non-ASCII characters.
Text is broken into lines of 78 character.
Fluid adds quotation marks before and after every line of text.
A list of control characters and ", ', and \\ are escaped by adding a \\ in
front of them. Escape ?? by writing ?\\?. All other characters that are not
between 32 and 126 inclusive will be escaped as octal characters.
This function is utf8 agnostic.
\param[in] text write this string
*/
void Code_Writer::write_cstring(fluid::string_view text)
{
const char *next_line = "\"\n\"";
// Handle sourcecode preview
if (write_codeview) {
if (text.data() == nullptr) {
// In code-view mode, the text is not always available, so just print a placeholder.
crc_puts("\" ... text... \"");
return;
} else if (text.size() > 300) {
// If the text is too long, just print a placeholder with the size of the text.
crc_puts("\" ... " + std::to_string(text.size()) + " bytes of text... \"");
return;
} // else fall through and render the text
}
// If there is no input data, write an error statement.
if (text.data() == nullptr) {
crc_puts("\n#error string not found\n");
crc_puts("nullptr");
return;
}
// Write the escaped text at 78 characters per line.
const char *p = text.data();
const char *e = text.data()+text.size();
int linelength = 1;
crc_putc('\"');
for (; p < e;) {
int c = *p++;
switch (c) {
case '\b': c = 'b'; goto QUOTED;
case '\t': c = 't'; goto QUOTED;
case '\n': c = 'n'; goto QUOTED;
case '\f': c = 'f'; goto QUOTED;
case '\r': c = 'r'; goto QUOTED;
case '\"':
case '\'':
case '\\':
QUOTED:
if (linelength >= 77) { crc_puts(next_line); linelength = 0; }
crc_putc('\\');
crc_putc(c);
linelength += 2;
break;
case '?': // prevent trigraphs by writing ?? as ?\?
if ( (p-2 >= text.data()) && (*(p-2) == '?')) {
// first question mark was alreaady written
crc_putc('\\');
crc_putc('?');
linelength += 2;
break;
}
// else fall through:
default:
if (c >= ' ' && c < 127) {
// a legal ASCII character
if (linelength >= 78) { crc_puts(next_line); linelength = 0; }
crc_putc(c);
linelength++;
break;
}
// if the UTF-8 option is checked, write unicode characters verbatim
if (proj_.utf8_in_src && (c&0x80)) {
if ((c&0x40)) {
// This is the first character in a utf-8 sequence (0b11......).
// A line break would be ok here. Do not put linebreak in front of
// following characters (0b10......)
if (linelength >= 78) { crc_puts(next_line); linelength = 0; }
linelength++;
}
crc_putc(c);
break;
}
// otherwise we must print it as an octal constant:
c &= 255;
if (linelength >= 74) { crc_puts(next_line); linelength = 0; }
char buf[8];
snprintf(buf, sizeof(buf), "\\%03o", c);
crc_puts(buf);
linelength += 4;
break;
}
}
crc_putc('\"');
}
/**
Write an array of C binary data (does not add a null).
The output is bracketed in { and }. The content is written
as decimal bytes, i.e. `{ 1, 2, 200 }`
\param[in] block pointer to a block of binary data, interpreted as unsigned bytes
*/
void Code_Writer::write_cdata(fluid::string_view block)
{
if (write_codeview) {
if (!block.empty())
crc_puts("{ /* ... " + std::to_string(block.size()) + " bytes of binary data... */ }");
else
crc_puts("{ /* ... binary data... */ }");
return;
}
if (block.data() == nullptr) {
crc_puts("\n#error data not found\n");
crc_puts("{ /* ... undefined size binary data... */ }");
return;
}
const unsigned char *w = (const unsigned char *)block.data();
const unsigned char *e = w+block.size();
int linelength = 1;
crc_putc('{');
for (; w < e;) {
unsigned char c = *w++;
if (c>99) linelength += 4;
else if (c>9) linelength += 3;
else linelength += 2;
if (linelength >= 77) {
crc_puts("\n");
linelength = 0;
}
crc_puts(std::to_string(c));
if (w<e) crc_putc(',');
}
crc_putc('}');
}
/**
Write code to the source file.
\param[in] code string containing the code to write
*/
void Code_Writer::write_c(const std::string& code) {
crc_puts(code);
}
/**
Write a line of code to the source file, appending an optional comment.
if the code line does not end in a ';' or '}', a ';' will be added.
\param[in] indent indentation string for all lines
\param[in] code line of code
\param[in] comment optional commentary
*/
void Code_Writer::write_cc(const std::string& indent, const std::string& code, const std::string& comment) {
write_c(indent + code);
char cc = code.back();
if (cc!='}' && cc!=';')
write_c(";");
if (!comment.empty())
write_c(" " + comment);
write_c("\n");
}
/**
Write code to the header file.
\param[in] code string containing the code to write
*/
void Code_Writer::write_h(const std::string& code) {
header_buffer << code;
}
/**
Write a line of code to the header file, appending an optional comment.
if the code line does not end in a ';' or '}', a ';' will be added.
\param[in] indent indentation string for all lines
\param[in] code line of code
\param[in] comment optional commentary
*/
void Code_Writer::write_hc(const std::string& indent, const std::string& code, const std::string& comment) {
write_h(indent + code);
char cc = code.back();
if (cc!='}' && cc!=';')
write_h(";");
if (!comment.empty())
write_h(" " + comment);
write_h("\n");
}
/**
Write a block of code, indenting it as needed.
\param[in] codeblock one or more lines of text, separated by \\n
\param[in] additional_indent increment indentation by this amount
\param[in] trail_char append this character if the last line did not end with
a newline, usually 0 or newline.
*/
void Code_Writer::write_c_indented(const std::string& codeblock, int additional_indent, char trail_char)
{
if (codeblock.empty()) return;
std::string trailing_text;
if (trail_char && codeblock.back() != '\n') {
trailing_text = std::string(1, trail_char);
}
indentation += additional_indent;
size_t line_start = 0;
size_t line_end = 0;
size_t line_length = 0;
bool done = false;
while (!done)
{
// get the length of the next line.
line_end = codeblock.find('\n', line_start);
if (line_end == std::string::npos) {
line_length = codeblock.length() - line_start;
done = true;
if (line_length == 0) break;
} else {
line_length = line_end - line_start + 1; // include the newline character
}
if (codeblock[line_start] == '\n') {
// no characters in the line, don't indent
write_c("\n");
} else if (codeblock[line_start]=='#') {
// don't indent preprocessor statments starting with '#'
write_c(codeblock.substr(line_start, line_length));
} else {
// indent all other text lines
write_c(indent() + codeblock.substr(line_start, line_length));
}
line_start = line_end + 1;
}
if (!trailing_text.empty()) {
write_c(trailing_text);
}
indentation -= additional_indent;
}
/**
Return true if the type can be a member of a class.
Some types are treated differently if they are inside class. Especially within
a Widget Class, children that are widgets are written as part of the
constructor whereas functions, declarations, and inline data are seen as
members of the class itself.
*/
bool is_class_member(Node *t) {
return dynamic_cast<Function_Node*>(t)
|| dynamic_cast<Decl_Node*>(t)
|| dynamic_cast<Data_Node*>(t)
|| dynamic_cast<Class_Node*>(t) // Caution, class in a class mostly untested
|| dynamic_cast<Widget_Class_Node*>(t) // Caution, class in a class mostly untested
// || dynamic_cast<DeclBlock_Node*>(t) // Declaration blocks are generally not handled well
;
}
/**
Return true, if this is a comment, and if it is followed by a class member.
This must only be called if q is inside a widget class.
Widget classes can have widgets and members (functions/methods, declarations,
etc.) intermixed.
\param[in] q should be a comment type
\return true if this comment is followed by a class member
\return false if it is followed by a widget or code
\see is_class_member(Node *t)
*/
static bool is_comment_before_class_member(Node *q) {
if (dynamic_cast<Comment_Node*>(q) && q->next && q->next->level==q->level) {
if (dynamic_cast<Comment_Node*>(q->next))
return is_comment_before_class_member(q->next);
if (is_class_member(q->next))
return true;
}
return false;
}
/**
Recursively write static code and declarations
\param[in] p write this type and all its children
\return pointer to the next sibling
*/
Node* Code_Writer::write_static(Node* p) {
mark_start(p->static_data);
p->write_static(*this);
mark_end(p->static_data);
Node* q;
for (q = p->next; q && q->level > p->level;) {
q = write_static(q);
}
p->write_static_after(*this);
return q;
}
/**
Recursively write code, putting children between the two parts of the parent code.
\param[in] p write this node and all its children
\return pointer to the next sibling
*/
Node* Code_Writer::write_code(Node* p) {
// write all code that comes before the children code
// (but don't write the last comment until the very end)
if (!(p==Fluid.proj.tree.last && dynamic_cast<Comment_Node*>(p))) {
mark_start(p->setup_node);
p->write_code1(*this);
mark_end(p->setup_node);
}
// recursively write the code of all children
Node* q;
if (dynamic_cast<Widget_Class_Node*>(p)) {
// Legacy handling for Widget_Class_Node:
// As the name suggests, these are widgets and classes at the same time.
// The implementation is a class that generates a collection of widgets
// in a Fl_Group of Fl_Window (or derived).
// Problem is, widgets are created directly as children of Widget_Class_Node
// instead of inside a constructor. So below we have to treat all children
// that are widgets in write_code1(), and all children that are regular
// class members (Methods, Variables) *after* write_code2().
// \todo Widgets should be required to be in a constructor
for (q = p->next; q && q->level > p->level;) {
// note: maybe declaration blocks should be handled like comments in the context
if (!is_class_member(q) && !is_comment_before_class_member(q)) {
q = write_code(q);
} else {
int level = q->level;
do {
q = q->next;
} while (q && q->level > level);
}
}
// write all code that come after the children
mark_start(p->finalize_node);
p->write_code2(*this);
mark_end(p->finalize_node);
for (q = p->next; q && q->level > p->level;) {
if (is_class_member(q) || is_comment_before_class_member(q)) {
q = write_code(q);
} else {
int level = q->level;
do {
q = q->next;
} while (q && q->level > level);
}
}
write_h("};\n");
class_stack.pop_back();
} else {
for (q = p->next; q && q->level > p->level;) q = write_code(q);
// write all code that come after the children
mark_start(p->finalize_node);
p->write_code2(*this);
mark_end(p->finalize_node);
}
return q;
}
/**
Write source code and header files.
If the files already exist, they will be overwritten only if the content
has changed. This conservative approach helps reduce unnecessary recompilation.
\note There is no true error checking here.
\param[in] s filename of source code file
\param[in] t filename of the header file
\return 0 if the operation failed, 1 if it was successful
*/
int Code_Writer::write_code(const std::string& code_arg, const std::string& header_arg, bool to_codeview) {
code_filename = code_arg;
header_filename = header_arg;
write_codeview = to_codeview;
// Remember the last code file location for MergeBack
if (!code_arg.empty() && proj_.write_mergeback_data && !to_codeview)
remember_mergeback_paths();
// Write the prologue comment, which may be a copyright notice or other
Node* first_node = write_prologue_comment();
write_prologue();
write_i18n_prologue();
for (Node* p = first_node; p;) {
// write all static data for this & all children first
write_static(p);
// then write the nested code:
p = write_code(p);
}
write_epilogue();
write_epilogue_comment();
// For codeview mode, strings are available via code_string() / header_string()
if (write_codeview)
return 1;
else
return flush();
}
/**
If the first node in the project is a comment, emit it before the actual prologue.
\return the node after the comment (or the first node if it was not a comment)
*/
Node* Code_Writer::write_prologue_comment() {
// if the first entry in the Type tree is a comment, then it is probably
// a copyright notice. We print that before anything else in the file!
Node* first_node = Fluid.proj.tree.first;
if (first_node && dynamic_cast<Comment_Node*>(first_node)) {
mark_start(first_node->setup_node);
mark_start(first_node->finalize_node);
// it is ok to write non-recursive code here, because comments have no children or code2 blocks
first_node->write_code1(*this);
mark_end(first_node->setup_node);
mark_end(first_node->finalize_node);
first_node = first_node->next;
}
return first_node;
}
/**
Write the prologue of the source and header files, including the include guard
and any necessary includes.
*/
void Code_Writer::write_prologue()
{
char version[128];
fl_snprintf(version, sizeof(version),
"// generated by Fast Light User Interface Designer (fluid) version %.4f\n\n",
FL_VERSION);
write_h(std::string(version));
crc_puts(version);
std::string guard = header_guard_macro();
write_h("#ifndef " + guard + "\n");
write_h("#define " + guard + "\n");
if (proj_.avoid_early_includes==0) {
write_h_once("#include <FL/Fl.H>");
}
if (!header_filename.empty() && proj_.include_H_from_C) {
if (write_codeview) {
write_c("#include \"CodeView.h\"\n");
} else if (proj_.header_file_name[0] == '.' && strchr(proj_.header_file_name.c_str(), '/') == nullptr) {
write_c("#include \"" + fl_filename_name_str(header_filename) + "\"\n");
} else {
write_c("#include \"" + proj_.header_file_name + "\"\n");
}
}
}
/**
Write the internationalization prologue, including any necessary includes and macros.
*/
void Code_Writer::write_i18n_prologue()
{
std::string loc_include, loc_conditional;
if (proj_.i18n.type==fluid::I18n_Type::GNU) {
loc_include = proj_.i18n.gnu_include;
loc_conditional = proj_.i18n.gnu_conditional;
} else {
loc_include = proj_.i18n.posix_include;
loc_conditional = proj_.i18n.posix_conditional;
}
if ((proj_.i18n.type != fluid::I18n_Type::NONE) && !loc_include.empty()) {
int conditional = !loc_conditional.empty();
if (conditional) {
write_c("#ifdef " + loc_conditional + "\n");
indentation++;
}
if (loc_include[0] != '<' && loc_include[0] != '\"')
write_c("#" + indent() + "include \"" + loc_include + "\"\n");
else
write_c("#" + indent() + "include " + loc_include + "\n");
if (proj_.i18n.type == fluid::I18n_Type::POSIX) {
if (!proj_.i18n.posix_file.empty()) {
write_c("extern nl_catd " + proj_.i18n.posix_file + ";\n");
} else {
write_c("// Initialize I18N stuff now for menus...\n");
write_c("#" + indent() + "include <locale.h>\n");
write_c("static char* _locale = setlocale(LC_MESSAGES, \"\");\n");
write_c("static nl_catd _catalog = catopen(\"" + proj_.basename() + "\", 0);\n");
}
}
if (conditional) {
write_c("#else\n");
if (proj_.i18n.type == fluid::I18n_Type::GNU) {
if (!proj_.i18n.gnu_function.empty()) {
write_c("#" + indent() + "ifndef " + proj_.i18n.gnu_function + "\n");
write_c("#" + indent() + "define " + proj_.i18n.gnu_function + "(text) text\n");
write_c("#" + indent() + "endif\n");
}
}
if (proj_.i18n.type == fluid::I18n_Type::POSIX) {
write_c("#" + indent() + "ifndef catgets\n");
write_c("#" + std::string(indent_plus(1)) + "define catgets(catalog, set, msgid, text) text\n");
write_c("#" + indent() + "endif\n");
}
indentation--;
write_c("#endif\n");
}
if (proj_.i18n.type == fluid::I18n_Type::GNU && proj_.i18n.gnu_static_function[0]) {
write_c("#ifndef " + proj_.i18n.gnu_static_function + "\n");
write_c("#" + std::string(indent_plus(1)) + "define " + proj_.i18n.gnu_static_function + "(text) text\n");
write_c("#endif\n");
}
}
}
/**
Write the epilogue of the source and header files, including the include guard.
*/
void Code_Writer::write_epilogue()
{
std::string guard = header_guard_macro();
write_h("#endif // " + guard + "\n");
}
/**
If the last node in the project is a comment, emit it after the actual epilogue.
*/
void Code_Writer::write_epilogue_comment() {
Node* last_node = Fluid.proj.tree.last;
if (last_node && (last_node != Fluid.proj.tree.first) && dynamic_cast<Comment_Node*>(last_node)) {
mark_start(last_node->setup_node);
mark_start(last_node->finalize_node);
last_node->write_code1(*this);
mark_end(last_node->setup_node);
mark_end(last_node->finalize_node);
}
}
/**
Write the contents of code_buffer and header_buffer..
If the files already exist, they will be overwritten only if the content
has changed. This conservative approach helps reduce unnecessary recompilation.
\return 0 if the operation failed, 1 if it was successful
*/
int Code_Writer::flush()
{
// Write code output: to file if filename provided, to stdout otherwise
bool code_ok = true;
if (!code_filename.empty()) {
code_ok = write_file_if_changed(code_filename, code_buffer.str());
} else {
fputs(code_buffer.str().c_str(), stdout);
}
// Write header output: to file if filename provided, to stdout otherwise
bool header_ok = true;
if (!header_filename.empty()) {
header_ok = write_file_if_changed(header_filename, header_buffer.str());
} else {
fputs(header_buffer.str().c_str(), stdout);
}
return code_ok && header_ok ? 1 : 0;
}
/**
Write the public/private/protected keywords inside the class.
This avoids repeating these words if the mode is already set.
\param[in] state 0 for private, 1 for public, 2 for protected
*/
void Code_Writer::write_public(int state)
{
if (class_stack.empty()) {
return;
}
auto* top = class_stack.back();
auto* current_class = dynamic_cast<Class_Node*>(top);
auto* current_widget_class = dynamic_cast<Widget_Class_Node*>(top);
if (current_class) {
if (current_class->write_public_state == state)
return;
current_class->write_public_state = state;
} else if (current_widget_class) {
if (current_widget_class->write_public_state == state)
return;
current_widget_class->write_public_state = state;
} else {
return;
}
switch (state) {
case 0: write_h("private:\n"); break;
case 1: write_h("public:\n"); break;
case 2: write_h("protected:\n"); break;
}
}
/**
Create and initialize a new C++ source code writer.
*/
Code_Writer::Code_Writer(Project &proj)
: proj_ { proj }
{
}
/**
Write a MergeBack tag as a separate line of C++ comment.
The tag contains information about the type of tag that we are writing, a
link back to the type using its unique id, and the CRC of all code written
after the previous tag up to this point.
\param[in] type FD_TAG_GENERIC, FD_TAG_CODE, FD_TAG_MENU_CALLBACK, or FD_TAG_WIDGET_CALLBACK
\param[in] uid the unique id of the current type
*/
void Code_Writer::tag(proj::Mergeback::Tag prev_type, proj::Mergeback::Tag next_type, unsigned short uid) {
if (proj_.write_mergeback_data) {
code_buffer << Mergeback::format_tag(prev_type, next_type, uid, crc_.value());
}
crc_.reset();
}
/** Write some text to the code buffer.
If MergeBack is enabled, the CRC calculation is continued.
\param[in] text any text, no requirements to end in a newline or such
\return always 0
*/
int Code_Writer::crc_puts(const std::string& text) {
if (proj_.write_mergeback_data) {
crc_.update(text);
}
code_buffer << text;
return 0;
}
/** Write a single ASCII character to the code buffer.
If MergeBack is enabled, the CRC calculation is continued.
\note to write UTF-8 characters, use Code_Writer::crc_puts(const char *text)
\param[in] c any character between 0 and 127 inclusive
\return the character written
*/
int Code_Writer::crc_putc(int c) {
if (proj_.write_mergeback_data) {
char cc = (char)c;
crc_.update(fluid::string_view((const char*)&cc, 1));
}
code_buffer << (char)c;
return c;
}
/**
Check if the content of a file matches a given string.
\param[in] filename path to the file to compare
\param[in] content the string content to compare with
\return true if the file exists and its content matches exactly, false otherwise
*/
bool Code_Writer::file_content_matches(const std::string& filename, const std::string &content) {
FILE *f = fl_fopen(filename.c_str(), "rb");
if (!f) {
return false; // File doesn't exist
}
// Get file size
if (fseek(f, 0, SEEK_END) != 0) {
fclose(f);
return false; // Seek error
}
long file_size = ftell(f);
if (file_size < 0) {
fclose(f);
return false; // ftell error
}
if (fseek(f, 0, SEEK_SET) != 0) {
fclose(f);
return false; // Seek error
}
// Quick check: if sizes don't match, content is different
if ((size_t)file_size != content.size()) {
fclose(f);
return false;
}
// Read file content and compare
std::string file_content((size_t)file_size, '\0');
size_t bytes_read = fread(&file_content[0], 1, (size_t)file_size, f);
fclose(f);
if (bytes_read != (size_t)file_size) {
return false; // Read error
}
return file_content == content;
}
/**
Write content to a file only if the content differs from the existing file.
This is a conservative write that avoids unnecessary file modifications,
which helps reduce recompilation in build systems.
\param[in] filename path to the file to write
\param[in] content the string content to write
\return true if the file was written or already up-to-date, false on write error
*/
bool Code_Writer::write_file_if_changed(const std::string& filename, const std::string &content) {
// If content matches, no need to write
if (file_content_matches(filename, content)) {
return true; // File is already up-to-date
}
// Content differs or file doesn't exist - write the new content
FILE *f = fl_fopen(filename.c_str(), "wb");
if (!f) {
return false; // Cannot open file for writing
}
size_t written = fwrite(content.c_str(), 1, content.size(), f);
int result = fclose(f);
return (written == content.size()) && (result == 0);
}
/**
Add a block of text to the CRC32 calculation.
Ignore leading whitespace and linefeed characters.
All spaces and tabs are treated as a single space.
This is to avoid differences in whitespace affecting the CRC, which can
happen when code blocks are indented while writing code.
This class can be used without the Code_Writer class, for example to
calculate a CRC of a string or file.
\param block The text to add to the CRC calculation.
*/
void fluid::CRC32::update(fluid::string_view block) {
const char *s = block.data();
size_t n = block.size();
for ( ; n>0; --n, ++s)
{
// don't count leading spaces and tabs in a line
if (line_start_) {
while (n>0 && fl_ascii_isspace(*s)) { s++; n--; }
if (n==0) return;
line_start_ = false;
}
if (*s=='\n') line_start_ = true;
// skip '\r' that may be introduced by Windows
if (*s=='\r') continue;
// skip multiple spaces in a row, but only if the previous character was a space
if (multi_space_) {
// skip all further spaces
if (fl_ascii_isspace(*s)) continue;
multi_space_ = false;
} else if (fl_ascii_isspace(*s)) {
multi_space_ = true;
crc_ = crc32(crc_, (const Bytef*)" ", 1);
continue;
}
crc_ = crc32(crc_, (const Bytef*)s, 1);
}
}
/**
Return the macro name to use for the include guard in the header file.
Creating the include guard is more involved than it seems at first glance.
The include guard is deduced from header filename. However, if the
filename contains unicode characters, they need to be encoded using
\Uxxxxxxxx or \\uxxxx encoding to form a valid macro identifier.
But that approach is not portable. Windows does not normalize Unicode
(ö is the letter \u00F6). macOS normalizes to NFD (ö is \u006F\u0308,
o followed by a Combining Diaresis ¨).
To make the include guard consistent across l=platforms, it can be
explicitly set by the user in the Project Settings.
*/
std::string Code_Writer::header_guard_macro()
{
if (!header_guard_macro_.empty()) {
return header_guard_macro_;
}
header_guard_macro_ = proj_.include_guard;
if (header_guard_macro_.empty()) {
std::ostringstream macro_name;
std::string header_name;
const char* a = nullptr;
if (write_codeview) {
header_name = proj_.headerfile_name();
a = header_name.c_str();
} else {
a = fl_filename_name(header_filename.c_str());
}
const char* b = a + strlen(a);
int len = 0;
unsigned ucs = fl_utf8decode(a, b, &len);
if (!fl_ascii_isalpha(ucs) && (ucs != '_'))
macro_name << '_';
while (a < b) {
ucs = fl_utf8decode(a, b, &len);
if (ucs > 0x0000ffff) { // large unicode character
macro_name << "\\U" << std::setw(8) << std::setfill('0') << std::hex << ucs;
} else if (ucs > 127) { // small unicode character or not an ASCI letter or digit
macro_name << "\\u" << std::setw(4) << std::setfill('0') << std::hex << ucs;
} else if (!fl_ascii_isalnum(ucs)) {
macro_name << '_';
} else {
macro_name << (char)ucs;
}
a += len;
}
header_guard_macro_ = macro_name.str();
}
return header_guard_macro_;
}
/**
Remember the last code file location for MergeBack.
This is stored in the user preferences, so that the next time the project is
opened, the code file can be found and merged back into the project.
*/
void Code_Writer::remember_mergeback_paths()
{
std::string filename = proj_.projectfile_path() + proj_.projectfile_name();
int i, n = (int)filename.size();
for (i=0; i<n; i++) if (filename[i]=='\\') filename[i] = '/';
Fl_Preferences build_records(Fl_Preferences::USER_L, "fltk.org", "fluid-build");
Fl_Preferences path(build_records, filename.c_str());
path.set("code", code_filename);
}
void Code_Writer::mark_start(TextSpan2& span) {
if (write_codeview) {
span.c.start = code_pos();
span.h.start = header_pos();
}
}
void Code_Writer::mark_end(TextSpan2& span) {
if (write_codeview) {
span.c.end = code_pos();
span.h.end = header_pos();
}
}
/**
Calculate the CRC32 of a block of text.
This is a convenience function that creates a CRC32 object, adds the block of
text, and returns the resulting CRC32 value.
\param block The text to calculate the CRC32 for.
\return The CRC32 value of the block of text.
*/
uint32_t fluid::CRC32::block(fluid::string_view block) {
fluid::CRC32 crc;
crc.update(block);
return crc.value();
}