From ad937750d754676f8e9e01ce2c1021ad1a80ee04 Mon Sep 17 00:00:00 2001 From: Linnnus Date: Sat, 17 Feb 2024 00:26:40 +0100 Subject: feat: Add creole utility This patch a) gives the different binaries' main() files more meaning full names and b) adds the creole binary, which works like smu except for my dialect of Creole (which should hopefully approach proper Creole as this repo matures). --- Makefile | 18 ++- flake.nix | 10 +- src/creole-test.c | 296 ------------------------------------------------- src/creole_test_main.c | 296 +++++++++++++++++++++++++++++++++++++++++++++++++ src/creole_util_main.c | 108 ++++++++++++++++++ src/main.c | 173 ----------------------------- src/simplewiki_main.c | 173 +++++++++++++++++++++++++++++ 7 files changed, 595 insertions(+), 479 deletions(-) delete mode 100644 src/creole-test.c create mode 100644 src/creole_test_main.c create mode 100644 src/creole_util_main.c delete mode 100644 src/main.c create mode 100644 src/simplewiki_main.c diff --git a/Makefile b/Makefile index 7520811..be87da4 100644 --- a/Makefile +++ b/Makefile @@ -11,30 +11,36 @@ PREFIX ?= /usr/local all: build/simplewiki -install: build/simplewiki +install: build/simplewiki build/creole mkdir -p $(PREFIX)/bin mkdir -p $(PREFIX)/share/man/man1 cp -f build/simplewiki $(PREFIX)/bin + cp -f build/creole $(PREFIX)/bin gzip $(PREFIX)/share/man/man1/simplewiki.1.gz uninstall: rm -f $(PREFIX)/bin/simplewiki + rm -f $(PREFIX)/bin/creole rm -f $(PREFIX)/share/man/man1/simplewiki.1.gz rmdir $(PREFIX)/bin >/dev/null 2>&1 || true rmdir $(PREFIX)/share/man/man1 >/dev/null 2>&1 || true -build/simplewiki: build/main.o build/die.o build/arena.o build/strutil.o build/creole.o - $(CC) $(CFLAGS) $(LDFLAGS) -o build/simplewiki $^ $(LDLIBS) +build/simplewiki: build/simplewiki_main.o build/die.o build/arena.o build/strutil.o build/creole.o + $(CC) $(CFLAGS) $(LDFLAGS) -o $@ $^ $(LDLIBS) -build/creole-test: build/creole-test.o build/creole.o +build/creole_test: build/creole_test_main.o build/creole.o $(CC) $(CFLAGS) -o $@ $^ -build/creole-test.o: src/creole-test.c -build/main.o: src/main.c src/arena.h src/die.h src/strutil.h src/creole.h +build/creole: build/creole_util_main.o build/creole.o + $(CC) $(CFLAGS) -o $@ $^ + +build/creole_test_main.o: src/creole_test_main.c +build/simplewiki_main.o: src/simplewiki_main.c src/arena.h src/die.h src/strutil.h src/creole.h build/arena.o: src/arena.c src/arena.h build/die.o: src/die.c src/die.h build/strutil.o: src/strutil.c src/strutil.h src/arena.h build/creole.o: src/creole.c +build/creole_util_main.o: src/creole_util_main.c src/creole.h build/%.o: src/%.c | build/ $(CC) $(CFLAGS) -c -o $@ $< diff --git a/flake.nix b/flake.nix index 1341dd9..ba4c2cd 100644 --- a/flake.nix +++ b/flake.nix @@ -39,14 +39,16 @@ src = ./.; - nativeBuildInputs = with pkgs; [ pkg-config ]; - buildInputs = with pkgs; [ libgit2 ]; + buildPhase = '' + make build/creole_test + ''; installPhase = '' mkdir -p $out/bin - make build/creole-test - mv build/creole-test $out/bin + mv build/creole_test $out/bin ''; + + meta.mainProgram = "creole_test"; }; devShell = (pkgs.mkShell.override { inherit stdenv; }) { diff --git a/src/creole-test.c b/src/creole-test.c deleted file mode 100644 index 1157721..0000000 --- a/src/creole-test.c +++ /dev/null @@ -1,296 +0,0 @@ -#include "creole.h" - -#include -#include - -#define COUNT(arr) (sizeof(arr)/sizeof((arr)[0])) - -#define strneq(a, b, n) (strncmp(a, b, n) == 0) - -struct { - const char *name, *input, *output; -} tests[] = { - { - .name = "Empty input produces no output", - .input = "", - .output = "" - }, - { - .name = "Basic paragraph markup", - .input = "Basic paragraph test with <, >, & and \"", - .output = "

Basic paragraph test with <, >, & and "

" - }, - { - .name = "Two paragraphs next to each other.", - .input = "Hello,\n\nworld!", - .output = "

Hello,

world!

" - }, - { - .name = "h1", - .input = "= Header =", - .output = "

Header

" - }, - { - .name = "h2", - .input = "== Header =", - .output = "

Header

" - }, - { - .name = "h3", - .input = "=== Header =", - .output = "

Header

" - }, - { - .name = "h4", - .input = "==== Header =", - .output = "

Header

" - }, - { - .name = "h5", - .input = "===== Header", - .output = "
Header
" - }, - { - .name = "h6", - .input = "====== Header =", - .output = "
Header
" - }, - { - .name = ">h6", - .input = "======= Header =", - .output = "

======= Header =

" - }, - { - .name = "Unnamed link", - .input = "[[MyPage]]", - .output = "

MyPage

" - }, - { - .name = "Named link", - .input = "[[MyPage|My page]]", - .output = "

My page

" - }, - { - .name = "Escaped link", - .input = "A paragraph with an ~[[escaped link]].", - .output = "

A paragraph with an [[escaped link]].

" - }, - { - .name = "Link with an escaped end", - .input = "[[https://example.com|A link with an escaped ~]] end]]", - .output = "

A link with an escaped ]] end

" - }, - { - .name = "Link with empty text", - .input = "[[https://example.com|]]", - .output = "

" - }, - { - .name = "Link with empty address", - .input = "[[|Hello]]", - .output = "

Hello

" - }, - { - .name = "Empty link", - .input = "[[]]", - .output = "

" - }, - { - .name = "Raw HTTP URL", - .input = "Here is a http://example.com/examplepage link.", - .output = "

Here is a " - "http://example.com/examplepage link.

" - }, - { // This is interesting because it doesn't contain a "://". - .name = "Raw mailto URL", - .input = "mailto:quandale@dingle.com", - .output = "

" - "mailto:quandale@dingle.com

" - }, - { // This test captures a non-standard (?) special case in the parser. - .name = "Raw URL followed by full stop", - .input = "My favorite website is https://wiki.c2.com/.", - .output = "

My favorite website is " - "https://wiki.c2.com/.

" - }, - { - .name = "Unnamed URL", - .input = "[[http //example.com/examplepage]]", - .output = "

" - "http //example.com/examplepage

" - }, - { - .name = "Named URL", - .input = "[[http //example.com/examplepage|Example Page]]", - .output = "

" - "Example Page

" - }, - { - .name = "Emphasis", - .input = "//Emphasis//", - .output = "

Emphasis

" - }, - { - .name = "Emphasis spans multiple lines", - .input = "Bold and italics should //be\nable// to cross lines.", - .output = "

Bold and italics should be\nable to cross lines.

" - }, - { - .name = "Emphasis does not cross paragraph boundaries", - .input = "This text should //not\n\nbe emphased// as it crosses a paragraph boundary.", - .output = "

This text should //not

" - "

be emphased// as it crosses a paragraph boundary.

" - }, - { - .name = "URL/emphasis ambiguity", - .input = "This is an //italic// text. This is a url " - "http://www.wikicreole.org. This is what can go wrong //this " - "should be an italic text//.", - .output = "

This is an italic text. This is a url " - "" - "http://www.wikicreole.org. This is what can go wrong " - "this should be an italic text.

" - }, -#if 0 - { - .name = "Simple unordered list", - .input = "* list item\n*list item 2", - .output = "
  • list item
  • \n
  • list item 2
" - }, - { - .name = "Simple ordered list", - .input = "# list item\n#list item 2", - .output = "
  1. list item
  2. \n
  3. list item 2
" - }, - { - .name = "Unordered item with unordered sublist", - .input = "* Item\n** Subitem", - .output = "
  • Item
      \n
    • Subitem
" - }, - { - .name = "Unordered sublist without initial tag", - .input = "** Sublist item", - .output = "

** Sublist item

" - }, - { - .name = "Ordered item with ordered sublist", - .input = "# Item\n## Subitem", - .output = "
  1. Item
      \n
    1. Subitem
" - }, - { - .name = "Ordered sublist without initial tag", - .input = "## Sublist item", - .output = "

## Sublist item

" - }, - { - .name = "Unordered item with ordered sublist", - .input = "* Item\n*# Subitem", - .output = "
  • Item
      \n
    1. Subitem
" - }, - { - .name = "Horizontal rule", - .input = "Some text\n----\nSome more text", - .output = "

Some text


Some more text

" - }, - { - .name = "Preformatted block", - .input = "{{{\nPreformatted block\n}}}", - .output = "
Preformatted block\n
" - }, - { - .name = "Two preformatted blocks", - .input = "{{{\nPreformatted block\n}}}\n{{{Block 2}}}", - .output = "
Preformatted block\n
Block 2
" - }, - { - .name = "Tables", - .input = "| A | B |\n| //C// | **D** \\\\ E |", - .output = "" - "" - "
A B
C D
E
" - }, - { - .name = "Image", - .input = "{{image.gif|my image}}", - .output = "

\"my

" - }, - { - .name = "Inline tt", - .input = "Inline {{{tt}}} example {{{here}}}!", - .output = "

Inline tt example here!

" - }, - { - .name = "Strong", - .input = "**Strong**", - .output = "

Strong

" - }, -#endif -}; - -int print_escaped(FILE *fp, const char *string, size_t length) { - static struct { - char from; - const char *to; - } replacements[] = { - { '\t', "\\t" }, - { '\n', "\\n" }, - { '"', "\\\"" }, - }; - - if (fputc('"', fp) == EOF) { - return -1; - } - - for (size_t i = 0; i < length; ++i) { - for (size_t j = 0; j < COUNT(replacements); ++j) { - if (string[i] == replacements[j].from) { - if (fprintf(fp, "%s", replacements[j].to) < 0) { - return -1; - } - goto next_char; - } - } - if (fputc(string[i], fp) == EOF) { - return -1; - } -next_char: - ; - } - - if (fputc('"', fp) == EOF) { - return -1; - } - - return 0; -} - -int print_escaped_ze(FILE *fp, const char *string) { - return print_escaped(fp, string, strlen(string)); -} - -int main(void) { - for (size_t i = 0; i < COUNT(tests); ++i) { - printf("Running test: \x1b[1m%s\x1b[0m... ", tests[i].name); - - static char buffer[1024]; - FILE *fp = fmemopen(buffer, sizeof(buffer), "wb"); - render_creole(fp, tests[i].input, strlen(tests[i].input)); - long buffer_length = ftell(fp); - fclose(fp); - - if (!strneq(buffer, tests[i].output, buffer_length)) { - printf("\x1b[31merror\x1b[0m\n"); - printf("├──── markup: "); - print_escaped_ze(stdout, tests[i].input); - putchar('\n'); - printf("├── expected: "); - print_escaped_ze(stdout, tests[i].output); - putchar('\n'); - printf("└─────── got: "); - print_escaped(stdout, buffer, buffer_length); - putchar('\n'); - } else { - printf("\x1b[32mok\x1b[0m\n"); - } - } -} diff --git a/src/creole_test_main.c b/src/creole_test_main.c new file mode 100644 index 0000000..1157721 --- /dev/null +++ b/src/creole_test_main.c @@ -0,0 +1,296 @@ +#include "creole.h" + +#include +#include + +#define COUNT(arr) (sizeof(arr)/sizeof((arr)[0])) + +#define strneq(a, b, n) (strncmp(a, b, n) == 0) + +struct { + const char *name, *input, *output; +} tests[] = { + { + .name = "Empty input produces no output", + .input = "", + .output = "" + }, + { + .name = "Basic paragraph markup", + .input = "Basic paragraph test with <, >, & and \"", + .output = "

Basic paragraph test with <, >, & and "

" + }, + { + .name = "Two paragraphs next to each other.", + .input = "Hello,\n\nworld!", + .output = "

Hello,

world!

" + }, + { + .name = "h1", + .input = "= Header =", + .output = "

Header

" + }, + { + .name = "h2", + .input = "== Header =", + .output = "

Header

" + }, + { + .name = "h3", + .input = "=== Header =", + .output = "

Header

" + }, + { + .name = "h4", + .input = "==== Header =", + .output = "

Header

" + }, + { + .name = "h5", + .input = "===== Header", + .output = "
Header
" + }, + { + .name = "h6", + .input = "====== Header =", + .output = "
Header
" + }, + { + .name = ">h6", + .input = "======= Header =", + .output = "

======= Header =

" + }, + { + .name = "Unnamed link", + .input = "[[MyPage]]", + .output = "

MyPage

" + }, + { + .name = "Named link", + .input = "[[MyPage|My page]]", + .output = "

My page

" + }, + { + .name = "Escaped link", + .input = "A paragraph with an ~[[escaped link]].", + .output = "

A paragraph with an [[escaped link]].

" + }, + { + .name = "Link with an escaped end", + .input = "[[https://example.com|A link with an escaped ~]] end]]", + .output = "

A link with an escaped ]] end

" + }, + { + .name = "Link with empty text", + .input = "[[https://example.com|]]", + .output = "

" + }, + { + .name = "Link with empty address", + .input = "[[|Hello]]", + .output = "

Hello

" + }, + { + .name = "Empty link", + .input = "[[]]", + .output = "

" + }, + { + .name = "Raw HTTP URL", + .input = "Here is a http://example.com/examplepage link.", + .output = "

Here is a " + "http://example.com/examplepage link.

" + }, + { // This is interesting because it doesn't contain a "://". + .name = "Raw mailto URL", + .input = "mailto:quandale@dingle.com", + .output = "

" + "mailto:quandale@dingle.com

" + }, + { // This test captures a non-standard (?) special case in the parser. + .name = "Raw URL followed by full stop", + .input = "My favorite website is https://wiki.c2.com/.", + .output = "

My favorite website is " + "https://wiki.c2.com/.

" + }, + { + .name = "Unnamed URL", + .input = "[[http //example.com/examplepage]]", + .output = "

" + "http //example.com/examplepage

" + }, + { + .name = "Named URL", + .input = "[[http //example.com/examplepage|Example Page]]", + .output = "

" + "Example Page

" + }, + { + .name = "Emphasis", + .input = "//Emphasis//", + .output = "

Emphasis

" + }, + { + .name = "Emphasis spans multiple lines", + .input = "Bold and italics should //be\nable// to cross lines.", + .output = "

Bold and italics should be\nable to cross lines.

" + }, + { + .name = "Emphasis does not cross paragraph boundaries", + .input = "This text should //not\n\nbe emphased// as it crosses a paragraph boundary.", + .output = "

This text should //not

" + "

be emphased// as it crosses a paragraph boundary.

" + }, + { + .name = "URL/emphasis ambiguity", + .input = "This is an //italic// text. This is a url " + "http://www.wikicreole.org. This is what can go wrong //this " + "should be an italic text//.", + .output = "

This is an italic text. This is a url " + "" + "http://www.wikicreole.org. This is what can go wrong " + "this should be an italic text.

" + }, +#if 0 + { + .name = "Simple unordered list", + .input = "* list item\n*list item 2", + .output = "
  • list item
  • \n
  • list item 2
" + }, + { + .name = "Simple ordered list", + .input = "# list item\n#list item 2", + .output = "
  1. list item
  2. \n
  3. list item 2
" + }, + { + .name = "Unordered item with unordered sublist", + .input = "* Item\n** Subitem", + .output = "
  • Item
      \n
    • Subitem
" + }, + { + .name = "Unordered sublist without initial tag", + .input = "** Sublist item", + .output = "

** Sublist item

" + }, + { + .name = "Ordered item with ordered sublist", + .input = "# Item\n## Subitem", + .output = "
  1. Item
      \n
    1. Subitem
" + }, + { + .name = "Ordered sublist without initial tag", + .input = "## Sublist item", + .output = "

## Sublist item

" + }, + { + .name = "Unordered item with ordered sublist", + .input = "* Item\n*# Subitem", + .output = "
  • Item
      \n
    1. Subitem
" + }, + { + .name = "Horizontal rule", + .input = "Some text\n----\nSome more text", + .output = "

Some text


Some more text

" + }, + { + .name = "Preformatted block", + .input = "{{{\nPreformatted block\n}}}", + .output = "
Preformatted block\n
" + }, + { + .name = "Two preformatted blocks", + .input = "{{{\nPreformatted block\n}}}\n{{{Block 2}}}", + .output = "
Preformatted block\n
Block 2
" + }, + { + .name = "Tables", + .input = "| A | B |\n| //C// | **D** \\\\ E |", + .output = "" + "" + "
A B
C D
E
" + }, + { + .name = "Image", + .input = "{{image.gif|my image}}", + .output = "

\"my

" + }, + { + .name = "Inline tt", + .input = "Inline {{{tt}}} example {{{here}}}!", + .output = "

Inline tt example here!

" + }, + { + .name = "Strong", + .input = "**Strong**", + .output = "

Strong

" + }, +#endif +}; + +int print_escaped(FILE *fp, const char *string, size_t length) { + static struct { + char from; + const char *to; + } replacements[] = { + { '\t', "\\t" }, + { '\n', "\\n" }, + { '"', "\\\"" }, + }; + + if (fputc('"', fp) == EOF) { + return -1; + } + + for (size_t i = 0; i < length; ++i) { + for (size_t j = 0; j < COUNT(replacements); ++j) { + if (string[i] == replacements[j].from) { + if (fprintf(fp, "%s", replacements[j].to) < 0) { + return -1; + } + goto next_char; + } + } + if (fputc(string[i], fp) == EOF) { + return -1; + } +next_char: + ; + } + + if (fputc('"', fp) == EOF) { + return -1; + } + + return 0; +} + +int print_escaped_ze(FILE *fp, const char *string) { + return print_escaped(fp, string, strlen(string)); +} + +int main(void) { + for (size_t i = 0; i < COUNT(tests); ++i) { + printf("Running test: \x1b[1m%s\x1b[0m... ", tests[i].name); + + static char buffer[1024]; + FILE *fp = fmemopen(buffer, sizeof(buffer), "wb"); + render_creole(fp, tests[i].input, strlen(tests[i].input)); + long buffer_length = ftell(fp); + fclose(fp); + + if (!strneq(buffer, tests[i].output, buffer_length)) { + printf("\x1b[31merror\x1b[0m\n"); + printf("├──── markup: "); + print_escaped_ze(stdout, tests[i].input); + putchar('\n'); + printf("├── expected: "); + print_escaped_ze(stdout, tests[i].output); + putchar('\n'); + printf("└─────── got: "); + print_escaped(stdout, buffer, buffer_length); + putchar('\n'); + } else { + printf("\x1b[32mok\x1b[0m\n"); + } + } +} diff --git a/src/creole_util_main.c b/src/creole_util_main.c new file mode 100644 index 0000000..7d29c68 --- /dev/null +++ b/src/creole_util_main.c @@ -0,0 +1,108 @@ +#include "creole.h" + +#include +#include +#include +#include +#include + +#define CHUNK_SIZE 128 + +int read_file(const char *file_path, char **out_buffer, size_t *out_length) { + assert(out_buffer != NULL && *out_buffer == NULL); + assert(file_path != NULL); + + FILE *fp = fopen(file_path, "r"); + if (fp == NULL) { + return -1; + } + + char *buffer = NULL; + size_t allocated = 0; + size_t used = 0; + while (true) { + // Grow buffer, if needed. + if (used + CHUNK_SIZE > allocated) { + // Grow exponentially to guarantee O(log(n)) performance. + allocated = (allocated == 0) ? CHUNK_SIZE : allocated * 2; + + // Overflow check. Some ANSI C compilers may optimize this away, though. + if (allocated <= used) { + free(buffer); + fclose(fp); + errno = EOVERFLOW; + return -1; + } + + char *temp = realloc(buffer, allocated); + if (temp == NULL) { + int old_errno = errno; + free(buffer); // free() may not set errno + fclose(fp); // fclose() may set errno + errno = old_errno; + return -1; + } + buffer = temp; + } + + size_t nread = fread(buffer + used, 1, CHUNK_SIZE, fp); + if (nread == 0) { + // End-of-file or errnor has occured. + // FIXME: Should we be checking (nread < CHUNK_SIZE)? + // https://stackoverflow.com/a/39322170 + break; + } + used += nread; + } + + if (ferror(fp)) { + int old_errno = errno; + free(buffer); // free() may not set errno + fclose(fp); // fclose() may set errno + errno = old_errno; + return -1; + } + + // Reallocate to optimal size. + char *temp = realloc(buffer, used + 1); + if (temp == NULL) { + int old_errno = errno; + free(buffer); // free() may not set errno + fclose(fp); // fclose() may set errno + errno = old_errno; + return -1; + } + buffer = temp; + + // Null-terminate the buffer. Note that buffers may still contain \0, + // so strlen(buffer) == length may not be true. + buffer[used] = '\0'; + + // Return buffer. + *out_buffer = buffer; + if (out_length != NULL) { + *out_length = used; + } + fclose(fp); + return 0; +} + +int main(void) { + size_t buffer_length = 0; + char *buffer = NULL; + if (read_file("/dev/stdin", &buffer, &buffer_length) < 0) { + perror("Failed to read stdin"); + return EXIT_FAILURE; + } + + render_creole(stdout, buffer, buffer_length); + + // The lack of return value makes it painfully obvious that we aren't + // handling errors at all. This represents my half-hearted attempt to fix that. + if (ferror(stdout)) { + perror("Failed to write to stdout"); + return EXIT_FAILURE; + } + + return EXIT_SUCCESS; +} diff --git a/src/main.c b/src/main.c deleted file mode 100644 index 5c04467..0000000 --- a/src/main.c +++ /dev/null @@ -1,173 +0,0 @@ -#include "arena.h" -#include "die.h" -#include "strutil.h" -#include "creole.h" - -// #include -#include // errno, EEXIST -#include // git_* -#include // false -#include -#include // EXIT_SUCCESS -#include // mkdir -#include // mode_t - -void xmkdir(const char *path, mode_t mode, bool exist_ok) { - if (mkdir(path, mode) < 0) { - if (exist_ok && errno == EEXIST) { - return; - } else { - die_errno("failed to mkdir %s", path); - } - } -} - -void process_other_file(const char *path, const char *source, size_t source_len) { - FILE *out = fopen(path, "w"); - printf("Copying: %s\n", path); - if (out == NULL) { - die_errno("failed to open %s for writing", path); - } - if (fwrite(source, 1, source_len, out) < source_len) { - die_errno("failed to write content to %s\n", path); - } - fclose(out); -} - -void process_markup_file(struct arena *a, const char *path, const char *source, size_t source_len) { - char *out_path = replace_suffix(a, path, ".txt", ".html"); - printf("Generating: %s\n", out_path); - FILE *out = fopen(out_path, "w"); - if (out == NULL) { - die_errno("failed to open %s for writing", path); - } - render_creole(out, source, source_len); - fclose(out); -} - -void process_dir(const char *path) { - xmkdir(path, 0755, false); -} - -void list_tree(struct arena *a, struct git_repository *repo, struct git_tree *tree, const char *prefix) { - // Grab a snapshot of the arena. - // All memory allocated within the arena in this subcalltree will be freed. - // This is effectively the same as allocating a new arena for each call to list_tree. - struct arena snapshot = *a; - - size_t tree_count = git_tree_entrycount(tree); - for (size_t i = 0; i < tree_count; ++i) { - // Read the entry. - const struct git_tree_entry *entry; - if ((entry = git_tree_entry_byindex(tree, i)) == NULL) { - die("read tree item"); - } - - // Construct path to entry. - const char *entry_out_path = joinpath(a, prefix, git_tree_entry_name(entry)); - - // entry->obj fail on submodules. just ignore them. - struct git_object *obj; - if (git_tree_entry_to_object(&obj, repo, entry) == 0) { - git_object_t type = git_object_type(obj); - switch (type) { - case GIT_OBJECT_BLOB: { - struct git_blob *blob = (struct git_blob *)obj; - const char *source = git_blob_rawcontent(blob); - if (source == NULL) { - die_git("get source for blob %s", git_oid_tostr_s(git_object_id(obj))); - } - size_t source_len = git_blob_rawsize(blob); - if (endswith(entry_out_path, ".txt") && !git_blob_is_binary(blob)) { - process_markup_file(a, entry_out_path, source, source_len); - } else { - process_other_file(entry_out_path, source, source_len); - } - git_object_free(obj); - } break; - case GIT_OBJECT_TREE: { - process_dir(entry_out_path); - list_tree(a, repo, (struct git_tree *)obj, entry_out_path); - git_object_free(obj); - } break; - default: { - // Ignore whatever weird thing this is. - git_object_free(obj); - } break; - } - } - } - - // Restore snapshot. - *a = snapshot; -} - -int main(int argc, char *argv[]) -{ - if (argc != 3) { - die("Usage: %s git-path out-path", argv[0]); - } - char *git_path = argv[1]; - char *out_path = argv[2]; - - // Initialize libgit. Note that calling git_libgit2_shutdown is not - // necessary, as per this snippet from the documentation: - // - // > Usually you don’t need to call the shutdown function as the operating - // > system will take care of reclaiming resources, but if your - // > application uses libgit2 in some areas which are not usually active, - // > you can use - // - // That's good news! - if (git_libgit2_init() < 0) { - die_git("initialize libgit"); - } - - // Do not search outside the git repository. GIT_CONFIG_LEVEL_APP is the highest level currently. - // for (int i = 1; i <= GIT_CONFIG_LEVEL_APP; i++) { - // if (git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, i, "") < 0) { - // die_git("set search path"); - // } - // } - - // Don't require the repository to be owned by the current user. - git_libgit2_opts(GIT_OPT_SET_OWNER_VALIDATION, 0); - - struct git_repository *repo; - if (git_repository_open_ext(&repo, git_path, GIT_REPOSITORY_OPEN_NO_SEARCH, NULL) < 0) { - die_git("open repository"); - } - - // Find HEAD. - struct git_object *head; - const struct git_oid *head_id; - if (git_revparse_single(&head, repo, "HEAD") < 0) { - die_git("parse HEAD"); - } - head_id = git_object_id(head); - git_object_free(head); - - // Get a handle to the tree at head. - struct git_commit *commit; - if (git_commit_lookup(&commit, repo, head_id) < 0) { - die_git("look up head commit"); - } - struct git_tree *tree; - if (git_commit_tree(&tree, commit) < 0) { - die_git("get tree for commit %s", git_oid_tostr_s(git_commit_id(commit))); - } - - // Create the initial output directory. - xmkdir(out_path, 0755, true); - - struct arena a = arena_create(1024); - list_tree(&a, repo, tree, out_path); -#ifndef NDEBUG - arena_destroy(&a); -#endif - -#ifndef NDEBUG - git_libgit2_shutdown(); -#endif - return EXIT_SUCCESS; -} diff --git a/src/simplewiki_main.c b/src/simplewiki_main.c new file mode 100644 index 0000000..5c04467 --- /dev/null +++ b/src/simplewiki_main.c @@ -0,0 +1,173 @@ +#include "arena.h" +#include "die.h" +#include "strutil.h" +#include "creole.h" + +// #include +#include // errno, EEXIST +#include // git_* +#include // false +#include +#include // EXIT_SUCCESS +#include // mkdir +#include // mode_t + +void xmkdir(const char *path, mode_t mode, bool exist_ok) { + if (mkdir(path, mode) < 0) { + if (exist_ok && errno == EEXIST) { + return; + } else { + die_errno("failed to mkdir %s", path); + } + } +} + +void process_other_file(const char *path, const char *source, size_t source_len) { + FILE *out = fopen(path, "w"); + printf("Copying: %s\n", path); + if (out == NULL) { + die_errno("failed to open %s for writing", path); + } + if (fwrite(source, 1, source_len, out) < source_len) { + die_errno("failed to write content to %s\n", path); + } + fclose(out); +} + +void process_markup_file(struct arena *a, const char *path, const char *source, size_t source_len) { + char *out_path = replace_suffix(a, path, ".txt", ".html"); + printf("Generating: %s\n", out_path); + FILE *out = fopen(out_path, "w"); + if (out == NULL) { + die_errno("failed to open %s for writing", path); + } + render_creole(out, source, source_len); + fclose(out); +} + +void process_dir(const char *path) { + xmkdir(path, 0755, false); +} + +void list_tree(struct arena *a, struct git_repository *repo, struct git_tree *tree, const char *prefix) { + // Grab a snapshot of the arena. + // All memory allocated within the arena in this subcalltree will be freed. + // This is effectively the same as allocating a new arena for each call to list_tree. + struct arena snapshot = *a; + + size_t tree_count = git_tree_entrycount(tree); + for (size_t i = 0; i < tree_count; ++i) { + // Read the entry. + const struct git_tree_entry *entry; + if ((entry = git_tree_entry_byindex(tree, i)) == NULL) { + die("read tree item"); + } + + // Construct path to entry. + const char *entry_out_path = joinpath(a, prefix, git_tree_entry_name(entry)); + + // entry->obj fail on submodules. just ignore them. + struct git_object *obj; + if (git_tree_entry_to_object(&obj, repo, entry) == 0) { + git_object_t type = git_object_type(obj); + switch (type) { + case GIT_OBJECT_BLOB: { + struct git_blob *blob = (struct git_blob *)obj; + const char *source = git_blob_rawcontent(blob); + if (source == NULL) { + die_git("get source for blob %s", git_oid_tostr_s(git_object_id(obj))); + } + size_t source_len = git_blob_rawsize(blob); + if (endswith(entry_out_path, ".txt") && !git_blob_is_binary(blob)) { + process_markup_file(a, entry_out_path, source, source_len); + } else { + process_other_file(entry_out_path, source, source_len); + } + git_object_free(obj); + } break; + case GIT_OBJECT_TREE: { + process_dir(entry_out_path); + list_tree(a, repo, (struct git_tree *)obj, entry_out_path); + git_object_free(obj); + } break; + default: { + // Ignore whatever weird thing this is. + git_object_free(obj); + } break; + } + } + } + + // Restore snapshot. + *a = snapshot; +} + +int main(int argc, char *argv[]) +{ + if (argc != 3) { + die("Usage: %s git-path out-path", argv[0]); + } + char *git_path = argv[1]; + char *out_path = argv[2]; + + // Initialize libgit. Note that calling git_libgit2_shutdown is not + // necessary, as per this snippet from the documentation: + // + // > Usually you don’t need to call the shutdown function as the operating + // > system will take care of reclaiming resources, but if your + // > application uses libgit2 in some areas which are not usually active, + // > you can use + // + // That's good news! + if (git_libgit2_init() < 0) { + die_git("initialize libgit"); + } + + // Do not search outside the git repository. GIT_CONFIG_LEVEL_APP is the highest level currently. + // for (int i = 1; i <= GIT_CONFIG_LEVEL_APP; i++) { + // if (git_libgit2_opts(GIT_OPT_SET_SEARCH_PATH, i, "") < 0) { + // die_git("set search path"); + // } + // } + + // Don't require the repository to be owned by the current user. + git_libgit2_opts(GIT_OPT_SET_OWNER_VALIDATION, 0); + + struct git_repository *repo; + if (git_repository_open_ext(&repo, git_path, GIT_REPOSITORY_OPEN_NO_SEARCH, NULL) < 0) { + die_git("open repository"); + } + + // Find HEAD. + struct git_object *head; + const struct git_oid *head_id; + if (git_revparse_single(&head, repo, "HEAD") < 0) { + die_git("parse HEAD"); + } + head_id = git_object_id(head); + git_object_free(head); + + // Get a handle to the tree at head. + struct git_commit *commit; + if (git_commit_lookup(&commit, repo, head_id) < 0) { + die_git("look up head commit"); + } + struct git_tree *tree; + if (git_commit_tree(&tree, commit) < 0) { + die_git("get tree for commit %s", git_oid_tostr_s(git_commit_id(commit))); + } + + // Create the initial output directory. + xmkdir(out_path, 0755, true); + + struct arena a = arena_create(1024); + list_tree(&a, repo, tree, out_path); +#ifndef NDEBUG + arena_destroy(&a); +#endif + +#ifndef NDEBUG + git_libgit2_shutdown(); +#endif + return EXIT_SUCCESS; +} -- cgit v1.2.3