apex-ruby 1.0.6 → 1.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/ext/apex_ext/apex_ext.c +6 -0
- data/ext/apex_ext/apex_src/AGENTS.md +41 -0
- data/ext/apex_ext/apex_src/CHANGELOG.md +412 -2
- data/ext/apex_ext/apex_src/CMakeLists.txt +41 -29
- data/ext/apex_ext/apex_src/Formula/apex.rb +2 -2
- data/ext/apex_ext/apex_src/Package.swift +9 -0
- data/ext/apex_ext/apex_src/README.md +31 -9
- data/ext/apex_ext/apex_src/ROADMAP.md +5 -0
- data/ext/apex_ext/apex_src/VERSION +1 -1
- data/ext/apex_ext/apex_src/cli/main.c +1125 -13
- data/ext/apex_ext/apex_src/docs/index.md +459 -0
- data/ext/apex_ext/apex_src/include/apex/apex.h +67 -5
- data/ext/apex_ext/apex_src/include/apex/ast_man.h +20 -0
- data/ext/apex_ext/apex_src/include/apex/ast_markdown.h +39 -0
- data/ext/apex_ext/apex_src/include/apex/ast_terminal.h +40 -0
- data/ext/apex_ext/apex_src/include/apex/module.modulemap +1 -1
- data/ext/apex_ext/apex_src/man/apex-config.5 +333 -258
- data/ext/apex_ext/apex_src/man/apex-config.5.md +3 -1
- data/ext/apex_ext/apex_src/man/apex-plugins.7 +401 -316
- data/ext/apex_ext/apex_src/man/apex.1 +663 -620
- data/ext/apex_ext/apex_src/man/apex.1.html +703 -0
- data/ext/apex_ext/apex_src/man/apex.1.md +160 -90
- data/ext/apex_ext/apex_src/objc/Apex.swift +6 -0
- data/ext/apex_ext/apex_src/objc/NSString+Apex.h +12 -0
- data/ext/apex_ext/apex_src/objc/NSString+Apex.m +9 -0
- data/ext/apex_ext/apex_src/pages/index.md +459 -0
- data/ext/apex_ext/apex_src/src/_README.md +4 -4
- data/ext/apex_ext/apex_src/src/apex.c +702 -44
- data/ext/apex_ext/apex_src/src/ast_json.c +1130 -0
- data/ext/apex_ext/apex_src/src/ast_json.h +46 -0
- data/ext/apex_ext/apex_src/src/ast_man.c +948 -0
- data/ext/apex_ext/apex_src/src/ast_markdown.c +409 -0
- data/ext/apex_ext/apex_src/src/ast_terminal.c +2516 -0
- data/ext/apex_ext/apex_src/src/extensions/abbreviations.c +8 -5
- data/ext/apex_ext/apex_src/src/extensions/definition_list.c +491 -1514
- data/ext/apex_ext/apex_src/src/extensions/definition_list.h +8 -15
- data/ext/apex_ext/apex_src/src/extensions/emoji.c +207 -0
- data/ext/apex_ext/apex_src/src/extensions/emoji.h +14 -0
- data/ext/apex_ext/apex_src/src/extensions/header_ids.c +178 -71
- data/ext/apex_ext/apex_src/src/extensions/highlight.c +37 -5
- data/ext/apex_ext/apex_src/src/extensions/ial.c +416 -47
- data/ext/apex_ext/apex_src/src/extensions/includes.c +241 -10
- data/ext/apex_ext/apex_src/src/extensions/includes.h +1 -0
- data/ext/apex_ext/apex_src/src/extensions/metadata.c +166 -3
- data/ext/apex_ext/apex_src/src/extensions/metadata.h +7 -0
- data/ext/apex_ext/apex_src/src/extensions/sup_sub.c +34 -3
- data/ext/apex_ext/apex_src/src/extensions/syntax_highlight.c +55 -10
- data/ext/apex_ext/apex_src/src/extensions/syntax_highlight.h +7 -4
- data/ext/apex_ext/apex_src/src/extensions/table_html_postprocess.c +84 -52
- data/ext/apex_ext/apex_src/src/extensions/toc.c +133 -19
- data/ext/apex_ext/apex_src/src/filters_ast.c +194 -0
- data/ext/apex_ext/apex_src/src/filters_ast.h +36 -0
- data/ext/apex_ext/apex_src/src/html_renderer.c +1265 -35
- data/ext/apex_ext/apex_src/src/html_renderer.h +21 -0
- data/ext/apex_ext/apex_src/src/plugins_remote.c +40 -14
- data/ext/apex_ext/apex_src/tests/CMakeLists.txt +1 -0
- data/ext/apex_ext/apex_src/tests/README.md +11 -5
- data/ext/apex_ext/apex_src/tests/fixtures/comprehensive_test.md +13 -2
- data/ext/apex_ext/apex_src/tests/fixtures/filters/filter_output_with_rawblock.json +1 -0
- data/ext/apex_ext/apex_src/tests/fixtures/filters/unwrap.md +7 -0
- data/ext/apex_ext/apex_src/tests/fixtures/images/auto-wildcard.md +8 -0
- data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu.avif +0 -0
- data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu.jpg +0 -0
- data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu.webp +0 -0
- data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu@2x.avif +0 -0
- data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu@2x.jpg +0 -0
- data/ext/apex_ext/apex_src/tests/fixtures/images/img/app-pass-1-profile-menu@2x.webp +0 -0
- data/ext/apex_ext/apex_src/tests/fixtures/images/media_formats_test.md +63 -0
- data/ext/apex_ext/apex_src/tests/fixtures/includes/data-semi.csv +3 -0
- data/ext/apex_ext/apex_src/tests/fixtures/includes/with space.txt +1 -0
- data/ext/apex_ext/apex_src/tests/fixtures/tables/inline_tables_test.md +4 -1
- data/ext/apex_ext/apex_src/tests/paginate_cli_test.sh +64 -0
- data/ext/apex_ext/apex_src/tests/terminal_width_test.sh +29 -0
- data/ext/apex_ext/apex_src/tests/test-swift-package.sh +14 -0
- data/ext/apex_ext/apex_src/tests/test_cmark_callback.c +189 -0
- data/ext/apex_ext/apex_src/tests/test_extensions.c +374 -0
- data/ext/apex_ext/apex_src/tests/test_metadata.c +68 -0
- data/ext/apex_ext/apex_src/tests/test_output.c +291 -2
- data/ext/apex_ext/apex_src/tests/test_runner.c +10 -0
- data/ext/apex_ext/apex_src/tests/test_syntax_highlight.c +1 -1
- data/ext/apex_ext/apex_src/tests/test_tables.c +17 -1
- data/lib/apex/version.rb +1 -1
- metadata +32 -2
- data/ext/apex_ext/apex_src/docs/FUTURE_FEATURES.md +0 -456
|
@@ -16,20 +16,164 @@
|
|
|
16
16
|
#include <dirent.h>
|
|
17
17
|
#include <sys/stat.h>
|
|
18
18
|
#include <sys/ioctl.h>
|
|
19
|
+
#include <limits.h>
|
|
19
20
|
|
|
20
|
-
/* Remote plugin directory helpers (from plugins_remote.c) */
|
|
21
|
+
/* Remote plugin directory helpers (from plugins_remote.c) */
|
|
21
22
|
typedef struct apex_remote_plugin apex_remote_plugin;
|
|
22
23
|
typedef struct apex_remote_plugin_list apex_remote_plugin_list;
|
|
23
24
|
|
|
24
25
|
apex_remote_plugin_list *apex_remote_fetch_directory(const char *url);
|
|
26
|
+
apex_remote_plugin_list *apex_remote_fetch_filters_directory(const char *url);
|
|
25
27
|
void apex_remote_print_plugins(apex_remote_plugin_list *list);
|
|
26
28
|
void apex_remote_print_plugins_filtered(apex_remote_plugin_list *list,
|
|
27
29
|
const char **installed_ids,
|
|
28
|
-
size_t installed_count
|
|
30
|
+
size_t installed_count,
|
|
31
|
+
const char *noun);
|
|
29
32
|
apex_remote_plugin *apex_remote_find_plugin(apex_remote_plugin_list *list, const char *id);
|
|
30
33
|
void apex_remote_free_plugins(apex_remote_plugin_list *list);
|
|
31
34
|
const char *apex_remote_plugin_repo(apex_remote_plugin *p);
|
|
32
35
|
|
|
36
|
+
/* Shared JSON helpers used for filters as well */
|
|
37
|
+
char *apex_remote_fetch_json(const char *url);
|
|
38
|
+
char *apex_remote_extract_string(const char *obj, const char *key);
|
|
39
|
+
|
|
40
|
+
/* ------------------------------------------------------------------------- */
|
|
41
|
+
/* Syntax highlighting theme listing */
|
|
42
|
+
/* ------------------------------------------------------------------------- */
|
|
43
|
+
|
|
44
|
+
static int apex_cli_terminal_width(void) {
|
|
45
|
+
struct winsize ws;
|
|
46
|
+
if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) {
|
|
47
|
+
return (int)ws.ws_col;
|
|
48
|
+
}
|
|
49
|
+
const char *cols_env = getenv("COLUMNS");
|
|
50
|
+
if (cols_env && *cols_env) {
|
|
51
|
+
long v = strtol(cols_env, NULL, 10);
|
|
52
|
+
if (v > 0 && v < INT_MAX) {
|
|
53
|
+
return (int)v;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return 80;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
static void apex_cli_print_theme_section(const char *title,
|
|
60
|
+
const char *const *items,
|
|
61
|
+
size_t count) {
|
|
62
|
+
fprintf(stdout, "%s\n", title);
|
|
63
|
+
fprintf(stdout, "----------------------------------------\n");
|
|
64
|
+
if (count == 0) {
|
|
65
|
+
fprintf(stdout, "(no themes)\n\n");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
size_t max_len = 0;
|
|
70
|
+
for (size_t i = 0; i < count; i++) {
|
|
71
|
+
size_t len = strlen(items[i]);
|
|
72
|
+
if (len > max_len) max_len = len;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
int term_width = apex_cli_terminal_width();
|
|
76
|
+
int col_width = (int)max_len + 2;
|
|
77
|
+
if (col_width <= 0) col_width = 16;
|
|
78
|
+
int max_cols = term_width > 0 ? term_width / col_width : 1;
|
|
79
|
+
if (max_cols < 1) max_cols = 1;
|
|
80
|
+
if (max_cols > 4) max_cols = 4;
|
|
81
|
+
int cols = max_cols;
|
|
82
|
+
int rows = (int)((count + (size_t)cols - 1) / (size_t)cols);
|
|
83
|
+
if (rows < 1) rows = 1;
|
|
84
|
+
|
|
85
|
+
for (int r = 0; r < rows; r++) {
|
|
86
|
+
for (int c = 0; c < cols; c++) {
|
|
87
|
+
size_t idx = (size_t)r + (size_t)rows * (size_t)c;
|
|
88
|
+
if (idx >= count) {
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
const char *name = items[idx];
|
|
92
|
+
int pad = col_width - (int)strlen(name);
|
|
93
|
+
if (pad < 1) pad = 1;
|
|
94
|
+
fprintf(stdout, "%s", name);
|
|
95
|
+
for (int p = 0; p < pad; p++) fputc(' ', stdout);
|
|
96
|
+
}
|
|
97
|
+
fputc('\n', stdout);
|
|
98
|
+
}
|
|
99
|
+
fputc('\n', stdout);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
static void apex_cli_print_highlight_themes(void) {
|
|
103
|
+
static const char *const pygments_themes[] = {
|
|
104
|
+
"abap",
|
|
105
|
+
"algol",
|
|
106
|
+
"algol_nu",
|
|
107
|
+
"arduino",
|
|
108
|
+
"autumn",
|
|
109
|
+
"bw",
|
|
110
|
+
"borland",
|
|
111
|
+
"coffee",
|
|
112
|
+
"colorful",
|
|
113
|
+
"default",
|
|
114
|
+
"dracula",
|
|
115
|
+
"emacs",
|
|
116
|
+
"friendly_grayscale",
|
|
117
|
+
"friendly",
|
|
118
|
+
"fruity",
|
|
119
|
+
"github-dark",
|
|
120
|
+
"gruvbox-dark",
|
|
121
|
+
"gruvbox-light",
|
|
122
|
+
"igor",
|
|
123
|
+
"inkpot",
|
|
124
|
+
"lightbulb",
|
|
125
|
+
"lilypond",
|
|
126
|
+
"lovelace",
|
|
127
|
+
"manni",
|
|
128
|
+
"material",
|
|
129
|
+
"monokai",
|
|
130
|
+
"murphy",
|
|
131
|
+
"native",
|
|
132
|
+
"nord-darker",
|
|
133
|
+
"nord",
|
|
134
|
+
"one-dark",
|
|
135
|
+
"paraiso-dark",
|
|
136
|
+
"paraiso-light",
|
|
137
|
+
"pastie",
|
|
138
|
+
"perldoc",
|
|
139
|
+
"rainbow_dash",
|
|
140
|
+
"rrt",
|
|
141
|
+
"sas",
|
|
142
|
+
"solarized-dark",
|
|
143
|
+
"solarized-light",
|
|
144
|
+
"staroffice",
|
|
145
|
+
"stata-dark",
|
|
146
|
+
"stata-light",
|
|
147
|
+
"tango",
|
|
148
|
+
"trac",
|
|
149
|
+
"vim",
|
|
150
|
+
"vs",
|
|
151
|
+
"xcode",
|
|
152
|
+
"zenburn"
|
|
153
|
+
};
|
|
154
|
+
static const char *const skylighting_themes[] = {
|
|
155
|
+
"kate",
|
|
156
|
+
"breezeDark",
|
|
157
|
+
"pygments",
|
|
158
|
+
"espresso",
|
|
159
|
+
"tango",
|
|
160
|
+
"haddock",
|
|
161
|
+
"monochrome",
|
|
162
|
+
"zenburn"
|
|
163
|
+
};
|
|
164
|
+
static const char *const shiki_themes[] = {
|
|
165
|
+
"Bundled themes: see https://shiki.style/themes",
|
|
166
|
+
"Special theme: none (disables highlighting)"
|
|
167
|
+
};
|
|
168
|
+
|
|
169
|
+
apex_cli_print_theme_section("Pygments themes", pygments_themes,
|
|
170
|
+
sizeof(pygments_themes) / sizeof(pygments_themes[0]));
|
|
171
|
+
apex_cli_print_theme_section("Skylighting themes", skylighting_themes,
|
|
172
|
+
sizeof(skylighting_themes) / sizeof(skylighting_themes[0]));
|
|
173
|
+
apex_cli_print_theme_section("Shiki themes", shiki_themes,
|
|
174
|
+
sizeof(shiki_themes) / sizeof(shiki_themes[0]));
|
|
175
|
+
}
|
|
176
|
+
|
|
33
177
|
/* ------------------------------------------------------------------------- */
|
|
34
178
|
/* Git helpers (mirrored from src/plugins.c for CLI-only use) */
|
|
35
179
|
/* */
|
|
@@ -469,17 +613,21 @@ static void print_usage(const char *program_name) {
|
|
|
469
613
|
fprintf(stderr, " --base-dir DIR Base directory for resolving relative paths (for images, includes, wiki links)\n");
|
|
470
614
|
fprintf(stderr, " --bibliography FILE Bibliography file (BibTeX, CSL JSON, or CSL YAML) - can be used multiple times\n");
|
|
471
615
|
fprintf(stderr, " --captions POSITION Table caption position: above or below (default: below)\n");
|
|
472
|
-
fprintf(stderr, " --code-highlight TOOL Use external tool for syntax highlighting (pygments, skylighting, or abbreviations p, s)\n");
|
|
616
|
+
fprintf(stderr, " --code-highlight TOOL Use external tool for syntax highlighting (pygments, skylighting, shiki, or abbreviations p, s, sh)\n");
|
|
617
|
+
fprintf(stderr, " --code-highlight-theme THEME Theme/style name for external syntax highlighters (tool-specific)\n");
|
|
618
|
+
fprintf(stderr, " --list-themes List available syntax highlighting themes for pygments, skylighting, and Shiki\n");
|
|
473
619
|
fprintf(stderr, " --code-line-numbers Include line numbers in syntax-highlighted code blocks (requires --code-highlight)\n");
|
|
474
620
|
fprintf(stderr, " --highlight-language-only Only highlight code blocks that have a language specified (requires --code-highlight)\n");
|
|
475
621
|
fprintf(stderr, " --combine Concatenate Markdown files (expanding includes) into a single Markdown stream\n");
|
|
476
622
|
fprintf(stderr, " When a SUMMARY.md file is provided, treat it as a GitBook index and combine\n");
|
|
477
623
|
fprintf(stderr, " the linked files in order. Output is raw Markdown suitable for piping back into Apex.\n");
|
|
478
624
|
fprintf(stderr, " --csl FILE Citation style file (CSL format)\n");
|
|
479
|
-
fprintf(stderr, " --css FILE, --style FILE Link to CSS file(s) in document head
|
|
480
|
-
fprintf(stderr, " Can be used multiple times or
|
|
625
|
+
fprintf(stderr, " --css FILE, --style FILE Link to CSS file(s) in document head. With HTML: requires -s/--standalone.\n");
|
|
626
|
+
fprintf(stderr, " With -t man-html -s: include custom CSS in the man page. Can be used multiple times or comma-separated (e.g., --css style.css)\n");
|
|
481
627
|
fprintf(stderr, " --embed-css Embed CSS file contents into a <style> tag in the document head (used with --css)\n");
|
|
482
628
|
fprintf(stderr, " --embed-images Embed local images as base64 data URLs in HTML output\n");
|
|
629
|
+
fprintf(stderr, " --[no-]image-captions Wrap images with title or alt text in <figure>/<figcaption> (default: on in unified/mmd)\n");
|
|
630
|
+
fprintf(stderr, " --[no-]title-captions-only Only add captions for images with title; alt-only images get no caption\n");
|
|
483
631
|
fprintf(stderr, " --hardbreaks Treat newlines as hard breaks\n");
|
|
484
632
|
fprintf(stderr, " --header-anchors Generate <a> anchor tags instead of header IDs\n");
|
|
485
633
|
fprintf(stderr, " -h, --help Show this help message\n");
|
|
@@ -488,6 +636,13 @@ static void print_usage(const char *program_name) {
|
|
|
488
636
|
fprintf(stderr, " --[no-]includes Enable file inclusion (enabled by default in unified mode)\n");
|
|
489
637
|
fprintf(stderr, " --indices Enable index processing (mmark, TextIndex, and Leanpub syntax)\n");
|
|
490
638
|
fprintf(stderr, " --install-plugin ID Install plugin by id from directory, or by Git URL/GitHub shorthand (user/repo)\n");
|
|
639
|
+
fprintf(stderr, " --list-filters List installed filters and available filters from the remote directory\n");
|
|
640
|
+
fprintf(stderr, " --install-filter ID Install AST filter by id from the central filters directory or by Git URL/GitHub shorthand\n");
|
|
641
|
+
fprintf(stderr, " --uninstall-filter ID Uninstall filter by id\n");
|
|
642
|
+
fprintf(stderr, " --filter NAME Run a single AST filter from ~/.config/apex/filters/NAME (Pandoc-style JSON filter)\n");
|
|
643
|
+
fprintf(stderr, " --filters Run all executable filters in ~/.config/apex/filters (sorted by name)\n");
|
|
644
|
+
fprintf(stderr, " --lua-filter FILE Run a Lua script as an AST filter via 'lua FILE' (Pandoc-style JSON filter)\n");
|
|
645
|
+
fprintf(stderr, " --no-strict-filters Do not abort on AST filter errors/invalid JSON; skip failing filters instead\n");
|
|
491
646
|
fprintf(stderr, " --link-citations Link citations to bibliography entries\n");
|
|
492
647
|
fprintf(stderr, " --list-plugins List installed plugins and available plugins from the remote directory\n");
|
|
493
648
|
fprintf(stderr, " --uninstall-plugin ID Uninstall plugin by id\n");
|
|
@@ -497,6 +652,7 @@ static void print_usage(const char *program_name) {
|
|
|
497
652
|
fprintf(stderr, " --mmd-merge Merge files from one or more mmd_merge-style index files into a single Markdown stream\n");
|
|
498
653
|
fprintf(stderr, " Index files list document parts line-by-line; indentation controls header level shifting.\n");
|
|
499
654
|
fprintf(stderr, " -m, --mode MODE Processor mode: commonmark, gfm, mmd, kramdown, unified (default)\n");
|
|
655
|
+
fprintf(stderr, " -t, --to FORMAT Output format: html (default), json (before filters), json-filtered/ast-json/ast (after filters), markdown/md, mmd, commonmark/cmark, kramdown, gfm, terminal/cli, terminal256, man, man-html\n");
|
|
500
656
|
fprintf(stderr, " --no-bibliography Suppress bibliography output\n");
|
|
501
657
|
fprintf(stderr, " --no-footnotes Disable footnote support\n");
|
|
502
658
|
fprintf(stderr, " --no-ids Disable automatic header ID generation\n");
|
|
@@ -509,6 +665,7 @@ static void print_usage(const char *program_name) {
|
|
|
509
665
|
fprintf(stderr, " --no-smart Disable smart typography\n");
|
|
510
666
|
fprintf(stderr, " --no-sup-sub Disable superscript/subscript syntax\n");
|
|
511
667
|
fprintf(stderr, " --[no-]divs Enable or disable Pandoc fenced divs (Unified mode only)\n");
|
|
668
|
+
fprintf(stderr, " --[no-]one-line-definitions Enable or disable one-line definition lists (Term :: Definition)\n");
|
|
512
669
|
fprintf(stderr, " --[no-]spans Enable or disable bracketed spans [text]{IAL} (Pandoc-style, enabled by default in unified mode)\n");
|
|
513
670
|
fprintf(stderr, " --no-tables Disable table support\n");
|
|
514
671
|
fprintf(stderr, " --no-transforms Disable metadata variable transforms\n");
|
|
@@ -520,13 +677,15 @@ static void print_usage(const char *program_name) {
|
|
|
520
677
|
fprintf(stderr, " --[no-]progress Show progress indicator during processing (enabled by default for TTY)\n");
|
|
521
678
|
fprintf(stderr, " --plugins Enable external/plugin processing\n");
|
|
522
679
|
fprintf(stderr, " --pretty Pretty-print HTML with indentation and whitespace\n");
|
|
680
|
+
fprintf(stderr, " --xhtml HTML5 output with self-closing void tags (<br />, <meta ... />)\n");
|
|
681
|
+
fprintf(stderr, " --strict-xhtml Polyglot XHTML/XML for parsers (xmlns, application/xhtml+xml meta; implies --xhtml). Mutually exclusive with --xhtml.\n");
|
|
523
682
|
fprintf(stderr, " --reject Reject all Critic Markup changes (revert edits)\n");
|
|
524
683
|
fprintf(stderr, " --[no-]relaxed-tables Enable or disable relaxed table parsing (no separator rows required)\n");
|
|
525
684
|
fprintf(stderr, " --[no-]per-cell-alignment Enable or disable per-cell alignment markers (colons at start/end of cells, enabled by default in unified mode)\n");
|
|
526
685
|
fprintf(stderr, " --script VALUE Inject <script> tags before </body> (standalone) or at end of HTML (snippet).\n");
|
|
527
686
|
fprintf(stderr, " VALUE can be a path, URL, or shorthand (mermaid, mathjax, katex). Can be used multiple times or as a comma-separated list.\n");
|
|
528
687
|
fprintf(stderr, " --show-tooltips Show tooltips on citations\n");
|
|
529
|
-
fprintf(stderr, " -s, --standalone Generate complete HTML document (with <html>, <head>, <body>)
|
|
688
|
+
fprintf(stderr, " -s, --standalone Generate complete HTML document (with <html>, <head>, <body>). For -t man-html, -s adds nav sidebar and full page; without -s, output is snippet only.\n");
|
|
530
689
|
fprintf(stderr, " --[no-]sup-sub Enable or disable MultiMarkdown-style superscript (^text^) and subscript (~text~) syntax\n");
|
|
531
690
|
fprintf(stderr, " --[no-]strikethrough Enable or disable GFM-style ~~strikethrough~~ processing\n");
|
|
532
691
|
fprintf(stderr, " --title TITLE Document title (requires --standalone, default: \"Document\")\n");
|
|
@@ -547,6 +706,9 @@ static void print_usage(const char *program_name) {
|
|
|
547
706
|
fprintf(stderr, " --wikilink-space MODE Space replacement for wiki links: dash, none, underscore, space (default: dash)\n");
|
|
548
707
|
fprintf(stderr, " --wikilink-extension EXT File extension to append to wiki links (e.g., html, md)\n");
|
|
549
708
|
fprintf(stderr, " --[no-]wikilink-sanitize Sanitize wiki link URLs (lowercase, remove apostrophes, etc.)\n");
|
|
709
|
+
fprintf(stderr, " --theme NAME Terminal theme name for -t terminal/terminal256 (from ~/.config/apex/terminal/themes/NAME.theme)\n");
|
|
710
|
+
fprintf(stderr, " --width N Hard-wrap terminal/terminal256 output at N visible columns\n");
|
|
711
|
+
fprintf(stderr, " -p, --paginate Page terminal/cli/terminal256 output through a pager (APEX_PAGER, then PAGER, then less -R)\n");
|
|
550
712
|
fprintf(stderr, "\n");
|
|
551
713
|
fprintf(stderr, "If no file is specified, reads from stdin.\n");
|
|
552
714
|
}
|
|
@@ -692,6 +854,121 @@ static int add_script_tag(char ***tags, size_t *count, size_t *capacity, const c
|
|
|
692
854
|
return 0;
|
|
693
855
|
}
|
|
694
856
|
|
|
857
|
+
/* Wrap ANSI-colored output to a fixed column width.
|
|
858
|
+
* This operates on the final rendered string and counts only visible
|
|
859
|
+
* characters toward the width, skipping over ANSI CSI sequences.
|
|
860
|
+
*/
|
|
861
|
+
static char *wrap_ansi_to_width(const char *input, int width) {
|
|
862
|
+
if (!input || width <= 0) {
|
|
863
|
+
return NULL;
|
|
864
|
+
}
|
|
865
|
+
|
|
866
|
+
size_t in_len = strlen(input);
|
|
867
|
+
/* Heuristic for output capacity: input length plus space for added newlines. */
|
|
868
|
+
size_t cap = in_len + (in_len / (size_t)width + 2) * 2 + 1;
|
|
869
|
+
char *out = malloc(cap);
|
|
870
|
+
if (!out) {
|
|
871
|
+
return NULL;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
size_t oi = 0;
|
|
875
|
+
int col = 0;
|
|
876
|
+
|
|
877
|
+
for (size_t i = 0; i < in_len; ) {
|
|
878
|
+
char c = input[i];
|
|
879
|
+
|
|
880
|
+
/* Newlines reset the column counter. */
|
|
881
|
+
if (c == '\n') {
|
|
882
|
+
if (oi + 1 >= cap) {
|
|
883
|
+
cap *= 2;
|
|
884
|
+
char *nb = realloc(out, cap);
|
|
885
|
+
if (!nb) {
|
|
886
|
+
free(out);
|
|
887
|
+
return NULL;
|
|
888
|
+
}
|
|
889
|
+
out = nb;
|
|
890
|
+
}
|
|
891
|
+
out[oi++] = c;
|
|
892
|
+
col = 0;
|
|
893
|
+
i++;
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
/* Simple handling for carriage return: pass through. */
|
|
898
|
+
if (c == '\r') {
|
|
899
|
+
if (oi + 1 >= cap) {
|
|
900
|
+
cap *= 2;
|
|
901
|
+
char *nb = realloc(out, cap);
|
|
902
|
+
if (!nb) {
|
|
903
|
+
free(out);
|
|
904
|
+
return NULL;
|
|
905
|
+
}
|
|
906
|
+
out = nb;
|
|
907
|
+
}
|
|
908
|
+
out[oi++] = c;
|
|
909
|
+
i++;
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
/* Preserve ANSI CSI sequences without counting them toward width. */
|
|
914
|
+
if (c == '\x1b' && i + 1 < in_len && input[i + 1] == '[') {
|
|
915
|
+
size_t start = i;
|
|
916
|
+
i += 2;
|
|
917
|
+
while (i < in_len && !((input[i] >= 'A' && input[i] <= 'Z') ||
|
|
918
|
+
(input[i] >= 'a' && input[i] <= 'z'))) {
|
|
919
|
+
i++;
|
|
920
|
+
}
|
|
921
|
+
if (i < in_len) {
|
|
922
|
+
i++; /* consume final letter */
|
|
923
|
+
}
|
|
924
|
+
size_t seq_len = i - start;
|
|
925
|
+
if (oi + seq_len + 1 >= cap) {
|
|
926
|
+
cap = cap + seq_len + 16;
|
|
927
|
+
char *nb = realloc(out, cap);
|
|
928
|
+
if (!nb) {
|
|
929
|
+
free(out);
|
|
930
|
+
return NULL;
|
|
931
|
+
}
|
|
932
|
+
out = nb;
|
|
933
|
+
}
|
|
934
|
+
memcpy(out + oi, input + start, seq_len);
|
|
935
|
+
oi += seq_len;
|
|
936
|
+
continue;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/* Insert a newline before adding another visible char if we've hit width. */
|
|
940
|
+
if (col >= width) {
|
|
941
|
+
if (oi + 1 >= cap) {
|
|
942
|
+
cap *= 2;
|
|
943
|
+
char *nb = realloc(out, cap);
|
|
944
|
+
if (!nb) {
|
|
945
|
+
free(out);
|
|
946
|
+
return NULL;
|
|
947
|
+
}
|
|
948
|
+
out = nb;
|
|
949
|
+
}
|
|
950
|
+
out[oi++] = '\n';
|
|
951
|
+
col = 0;
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
if (oi + 1 >= cap) {
|
|
955
|
+
cap *= 2;
|
|
956
|
+
char *nb = realloc(out, cap);
|
|
957
|
+
if (!nb) {
|
|
958
|
+
free(out);
|
|
959
|
+
return NULL;
|
|
960
|
+
}
|
|
961
|
+
out = nb;
|
|
962
|
+
}
|
|
963
|
+
out[oi++] = c;
|
|
964
|
+
col++;
|
|
965
|
+
i++;
|
|
966
|
+
}
|
|
967
|
+
|
|
968
|
+
out[oi] = '\0';
|
|
969
|
+
return out;
|
|
970
|
+
}
|
|
971
|
+
|
|
695
972
|
/**
|
|
696
973
|
* Normalize a plugin identifier to a Git repository URL.
|
|
697
974
|
* Returns a newly allocated string that must be freed by caller, or NULL on error.
|
|
@@ -1248,8 +1525,14 @@ int main(int argc, char *argv[]) {
|
|
|
1248
1525
|
bool plugins_cli_override = false;
|
|
1249
1526
|
bool plugins_cli_value = false;
|
|
1250
1527
|
bool list_plugins = false;
|
|
1528
|
+
bool list_themes = false;
|
|
1251
1529
|
const char *install_plugin_id = NULL;
|
|
1252
1530
|
const char *uninstall_plugin_id = NULL;
|
|
1531
|
+
bool list_filters = false;
|
|
1532
|
+
const char *uninstall_filter_id = NULL;
|
|
1533
|
+
|
|
1534
|
+
/* Filter install (AST filters) */
|
|
1535
|
+
const char *install_filter_id = NULL;
|
|
1253
1536
|
const char *input_file = NULL;
|
|
1254
1537
|
const char *output_file = NULL;
|
|
1255
1538
|
const char *meta_file = NULL;
|
|
@@ -1284,6 +1567,24 @@ int main(int argc, char *argv[]) {
|
|
|
1284
1567
|
size_t script_tag_count = 0;
|
|
1285
1568
|
size_t script_tag_capacity = 4;
|
|
1286
1569
|
|
|
1570
|
+
/* AST filters (Pandoc-style JSON filters) configured from CLI */
|
|
1571
|
+
char **ast_filter_names = NULL; /* Filter names from --filter (resolved in config dir) */
|
|
1572
|
+
size_t ast_filter_name_count = 0;
|
|
1573
|
+
size_t ast_filter_name_capacity = 4;
|
|
1574
|
+
bool run_all_filters_dir = false; /* --filters */
|
|
1575
|
+
bool ast_filters_strict = true; /* default strict mode; --no-strict-filters disables */
|
|
1576
|
+
|
|
1577
|
+
/* Lua filters: explicit script paths run via 'lua <script>' */
|
|
1578
|
+
char **lua_filter_paths = NULL;
|
|
1579
|
+
size_t lua_filter_count = 0;
|
|
1580
|
+
size_t lua_filter_capacity = 4;
|
|
1581
|
+
|
|
1582
|
+
/* Optional fixed-width wrapping for terminal output */
|
|
1583
|
+
int width_override = 0;
|
|
1584
|
+
|
|
1585
|
+
/* Pagination for terminal/terminal256 output */
|
|
1586
|
+
bool paginate_cli = false;
|
|
1587
|
+
|
|
1287
1588
|
/* Parse command-line arguments */
|
|
1288
1589
|
for (int i = 1; i < argc; i++) {
|
|
1289
1590
|
if (strcmp(argv[i], "-h") == 0 || strcmp(argv[i], "--help") == 0) {
|
|
@@ -1311,6 +1612,57 @@ int main(int argc, char *argv[]) {
|
|
|
1311
1612
|
fprintf(stderr, "Error: Unknown mode '%s'\n", argv[i]);
|
|
1312
1613
|
return 1;
|
|
1313
1614
|
}
|
|
1615
|
+
} else if (strcmp(argv[i], "-t") == 0 || strcmp(argv[i], "--to") == 0) {
|
|
1616
|
+
if (++i >= argc) {
|
|
1617
|
+
fprintf(stderr, "Error: --to requires an argument\n");
|
|
1618
|
+
return 1;
|
|
1619
|
+
}
|
|
1620
|
+
if (strcmp(argv[i], "html") == 0) {
|
|
1621
|
+
options.output_format = APEX_OUTPUT_HTML;
|
|
1622
|
+
} else if (strcmp(argv[i], "json") == 0) {
|
|
1623
|
+
options.output_format = APEX_OUTPUT_JSON;
|
|
1624
|
+
} else if (strcmp(argv[i], "json-filtered") == 0 || strcmp(argv[i], "ast-json") == 0 || strcmp(argv[i], "ast") == 0) {
|
|
1625
|
+
options.output_format = APEX_OUTPUT_JSON_FILTERED;
|
|
1626
|
+
} else if (strcmp(argv[i], "markdown") == 0 || strcmp(argv[i], "md") == 0) {
|
|
1627
|
+
options.output_format = APEX_OUTPUT_MARKDOWN;
|
|
1628
|
+
} else if (strcmp(argv[i], "mmd") == 0) {
|
|
1629
|
+
options.output_format = APEX_OUTPUT_MMD;
|
|
1630
|
+
} else if (strcmp(argv[i], "commonmark") == 0 || strcmp(argv[i], "cmark") == 0) {
|
|
1631
|
+
options.output_format = APEX_OUTPUT_COMMONMARK;
|
|
1632
|
+
} else if (strcmp(argv[i], "kramdown") == 0) {
|
|
1633
|
+
options.output_format = APEX_OUTPUT_KRAMDOWN;
|
|
1634
|
+
} else if (strcmp(argv[i], "gfm") == 0) {
|
|
1635
|
+
options.output_format = APEX_OUTPUT_GFM;
|
|
1636
|
+
} else if (strcmp(argv[i], "terminal") == 0 || strcmp(argv[i], "cli") == 0) {
|
|
1637
|
+
options.output_format = APEX_OUTPUT_TERMINAL;
|
|
1638
|
+
} else if (strcmp(argv[i], "terminal256") == 0) {
|
|
1639
|
+
options.output_format = APEX_OUTPUT_TERMINAL256;
|
|
1640
|
+
} else if (strcmp(argv[i], "man") == 0) {
|
|
1641
|
+
options.output_format = APEX_OUTPUT_MAN;
|
|
1642
|
+
} else if (strcmp(argv[i], "man-html") == 0) {
|
|
1643
|
+
options.output_format = APEX_OUTPUT_MAN_HTML;
|
|
1644
|
+
} else {
|
|
1645
|
+
fprintf(stderr, "Error: Unknown output format '%s'\n", argv[i]);
|
|
1646
|
+
fprintf(stderr, "Supported formats: html, json, json-filtered/ast-json/ast, markdown/md, mmd, commonmark/cmark, kramdown, gfm, terminal/cli, terminal256, man, man-html\n");
|
|
1647
|
+
return 1;
|
|
1648
|
+
}
|
|
1649
|
+
} else if (strcmp(argv[i], "--theme") == 0) {
|
|
1650
|
+
if (++i >= argc) {
|
|
1651
|
+
fprintf(stderr, "Error: --theme requires a name argument\n");
|
|
1652
|
+
return 1;
|
|
1653
|
+
}
|
|
1654
|
+
options.theme_name = argv[i];
|
|
1655
|
+
} else if (strcmp(argv[i], "--width") == 0) {
|
|
1656
|
+
if (++i >= argc) {
|
|
1657
|
+
fprintf(stderr, "Error: --width requires a column width argument\n");
|
|
1658
|
+
return 1;
|
|
1659
|
+
}
|
|
1660
|
+
width_override = atoi(argv[i]);
|
|
1661
|
+
if (width_override < 0) {
|
|
1662
|
+
width_override = 0;
|
|
1663
|
+
}
|
|
1664
|
+
} else if (strcmp(argv[i], "-p") == 0 || strcmp(argv[i], "--paginate") == 0) {
|
|
1665
|
+
paginate_cli = true;
|
|
1314
1666
|
} else if (strcmp(argv[i], "-o") == 0 || strcmp(argv[i], "--output") == 0) {
|
|
1315
1667
|
if (++i >= argc) {
|
|
1316
1668
|
fprintf(stderr, "Error: --output requires an argument\n");
|
|
@@ -1327,12 +1679,77 @@ int main(int argc, char *argv[]) {
|
|
|
1327
1679
|
plugins_cli_value = false;
|
|
1328
1680
|
} else if (strcmp(argv[i], "--list-plugins") == 0) {
|
|
1329
1681
|
list_plugins = true;
|
|
1682
|
+
} else if (strcmp(argv[i], "--list-themes") == 0) {
|
|
1683
|
+
list_themes = true;
|
|
1330
1684
|
} else if (strcmp(argv[i], "--install-plugin") == 0) {
|
|
1331
1685
|
if (++i >= argc) {
|
|
1332
1686
|
fprintf(stderr, "Error: --install-plugin requires an id argument\n");
|
|
1333
1687
|
return 1;
|
|
1334
1688
|
}
|
|
1335
1689
|
install_plugin_id = argv[i];
|
|
1690
|
+
} else if (strcmp(argv[i], "--list-filters") == 0) {
|
|
1691
|
+
list_filters = true;
|
|
1692
|
+
} else if (strcmp(argv[i], "--install-filter") == 0) {
|
|
1693
|
+
if (++i >= argc) {
|
|
1694
|
+
fprintf(stderr, "Error: --install-filter requires an id argument\n");
|
|
1695
|
+
return 1;
|
|
1696
|
+
}
|
|
1697
|
+
install_filter_id = argv[i];
|
|
1698
|
+
} else if (strcmp(argv[i], "--uninstall-filter") == 0) {
|
|
1699
|
+
if (++i >= argc) {
|
|
1700
|
+
fprintf(stderr, "Error: --uninstall-filter requires an id argument\n");
|
|
1701
|
+
return 1;
|
|
1702
|
+
}
|
|
1703
|
+
uninstall_filter_id = argv[i];
|
|
1704
|
+
} else if (strcmp(argv[i], "--filter") == 0) {
|
|
1705
|
+
if (++i >= argc) {
|
|
1706
|
+
fprintf(stderr, "Error: --filter requires a name argument\n");
|
|
1707
|
+
return 1;
|
|
1708
|
+
}
|
|
1709
|
+
/* Collect filter names; resolution to full paths happens later */
|
|
1710
|
+
if (!ast_filter_names) {
|
|
1711
|
+
ast_filter_names = malloc(ast_filter_name_capacity * sizeof(char *));
|
|
1712
|
+
if (!ast_filter_names) {
|
|
1713
|
+
fprintf(stderr, "Error: Memory allocation failed\n");
|
|
1714
|
+
return 1;
|
|
1715
|
+
}
|
|
1716
|
+
} else if (ast_filter_name_count >= ast_filter_name_capacity) {
|
|
1717
|
+
size_t new_cap = ast_filter_name_capacity ? ast_filter_name_capacity * 2 : 4;
|
|
1718
|
+
char **tmp = realloc(ast_filter_names, new_cap * sizeof(char *));
|
|
1719
|
+
if (!tmp) {
|
|
1720
|
+
fprintf(stderr, "Error: Memory allocation failed\n");
|
|
1721
|
+
return 1;
|
|
1722
|
+
}
|
|
1723
|
+
ast_filter_names = tmp;
|
|
1724
|
+
ast_filter_name_capacity = new_cap;
|
|
1725
|
+
}
|
|
1726
|
+
ast_filter_names[ast_filter_name_count++] = argv[i];
|
|
1727
|
+
} else if (strcmp(argv[i], "--filters") == 0) {
|
|
1728
|
+
run_all_filters_dir = true;
|
|
1729
|
+
} else if (strcmp(argv[i], "--no-strict-filters") == 0) {
|
|
1730
|
+
ast_filters_strict = false;
|
|
1731
|
+
} else if (strcmp(argv[i], "--lua-filter") == 0) {
|
|
1732
|
+
if (++i >= argc) {
|
|
1733
|
+
fprintf(stderr, "Error: --lua-filter requires a script path argument\n");
|
|
1734
|
+
return 1;
|
|
1735
|
+
}
|
|
1736
|
+
if (!lua_filter_paths) {
|
|
1737
|
+
lua_filter_paths = malloc(lua_filter_capacity * sizeof(char *));
|
|
1738
|
+
if (!lua_filter_paths) {
|
|
1739
|
+
fprintf(stderr, "Error: Memory allocation failed\n");
|
|
1740
|
+
return 1;
|
|
1741
|
+
}
|
|
1742
|
+
} else if (lua_filter_count >= lua_filter_capacity) {
|
|
1743
|
+
size_t new_cap = lua_filter_capacity ? lua_filter_capacity * 2 : 4;
|
|
1744
|
+
char **tmp = realloc(lua_filter_paths, new_cap * sizeof(char *));
|
|
1745
|
+
if (!tmp) {
|
|
1746
|
+
fprintf(stderr, "Error: Memory allocation failed\n");
|
|
1747
|
+
return 1;
|
|
1748
|
+
}
|
|
1749
|
+
lua_filter_paths = tmp;
|
|
1750
|
+
lua_filter_capacity = new_cap;
|
|
1751
|
+
}
|
|
1752
|
+
lua_filter_paths[lua_filter_count++] = argv[i];
|
|
1336
1753
|
} else if (strcmp(argv[i], "--uninstall-plugin") == 0) {
|
|
1337
1754
|
if (++i >= argc) {
|
|
1338
1755
|
fprintf(stderr, "Error: --uninstall-plugin requires an id argument\n");
|
|
@@ -1538,6 +1955,10 @@ int main(int argc, char *argv[]) {
|
|
|
1538
1955
|
options.document_title = argv[i];
|
|
1539
1956
|
} else if (strcmp(argv[i], "--pretty") == 0) {
|
|
1540
1957
|
options.pretty = true;
|
|
1958
|
+
} else if (strcmp(argv[i], "--xhtml") == 0) {
|
|
1959
|
+
options.xhtml = true;
|
|
1960
|
+
} else if (strcmp(argv[i], "--strict-xhtml") == 0) {
|
|
1961
|
+
options.strict_xhtml = true;
|
|
1541
1962
|
} else if (strcmp(argv[i], "--accept") == 0) {
|
|
1542
1963
|
options.enable_critic_markup = true;
|
|
1543
1964
|
options.critic_mode = 0; /* CRITIC_ACCEPT */
|
|
@@ -1586,7 +2007,7 @@ int main(int argc, char *argv[]) {
|
|
|
1586
2007
|
}
|
|
1587
2008
|
} else if (strcmp(argv[i], "--code-highlight") == 0) {
|
|
1588
2009
|
if (++i >= argc) {
|
|
1589
|
-
fprintf(stderr, "Error: --code-highlight requires a tool name (pygments, skylighting, or abbreviations p, s)\n");
|
|
2010
|
+
fprintf(stderr, "Error: --code-highlight requires a tool name (pygments, skylighting, shiki, or abbreviations p, s, sh)\n");
|
|
1590
2011
|
return 1;
|
|
1591
2012
|
}
|
|
1592
2013
|
/* Accept full names and abbreviations */
|
|
@@ -1594,10 +2015,19 @@ int main(int argc, char *argv[]) {
|
|
|
1594
2015
|
options.code_highlighter = "pygments";
|
|
1595
2016
|
} else if (strcmp(argv[i], "skylighting") == 0 || strcmp(argv[i], "s") == 0 || strcmp(argv[i], "sky") == 0) {
|
|
1596
2017
|
options.code_highlighter = "skylighting";
|
|
2018
|
+
} else if (strcmp(argv[i], "shiki") == 0 || strcmp(argv[i], "sh") == 0) {
|
|
2019
|
+
options.code_highlighter = "shiki";
|
|
1597
2020
|
} else {
|
|
1598
|
-
fprintf(stderr, "Error: --code-highlight tool must be 'pygments' (p)
|
|
2021
|
+
fprintf(stderr, "Error: --code-highlight tool must be 'pygments' (p), 'skylighting' (s), or 'shiki' (sh)\n");
|
|
1599
2022
|
return 1;
|
|
1600
2023
|
}
|
|
2024
|
+
} else if (strcmp(argv[i], "--code-highlight-theme") == 0 ||
|
|
2025
|
+
strcmp(argv[i], "--code-hilight-theme") == 0) {
|
|
2026
|
+
if (++i >= argc) {
|
|
2027
|
+
fprintf(stderr, "Error: --code-highlight-theme requires a theme name\n");
|
|
2028
|
+
return 1;
|
|
2029
|
+
}
|
|
2030
|
+
options.code_highlight_theme = argv[i];
|
|
1601
2031
|
} else if (strcmp(argv[i], "--code-line-numbers") == 0) {
|
|
1602
2032
|
options.code_line_numbers = true;
|
|
1603
2033
|
} else if (strcmp(argv[i], "--highlight-language-only") == 0) {
|
|
@@ -1622,6 +2052,10 @@ int main(int argc, char *argv[]) {
|
|
|
1622
2052
|
options.enable_divs = true;
|
|
1623
2053
|
} else if (strcmp(argv[i], "--no-divs") == 0) {
|
|
1624
2054
|
options.enable_divs = false;
|
|
2055
|
+
} else if (strcmp(argv[i], "--one-line-definitions") == 0) {
|
|
2056
|
+
options.enable_definition_lists = true;
|
|
2057
|
+
} else if (strcmp(argv[i], "--no-one-line-definitions") == 0) {
|
|
2058
|
+
options.enable_definition_lists = false;
|
|
1625
2059
|
} else if (strcmp(argv[i], "--spans") == 0) {
|
|
1626
2060
|
options.enable_spans = true;
|
|
1627
2061
|
} else if (strcmp(argv[i], "--no-spans") == 0) {
|
|
@@ -1848,12 +2282,23 @@ int main(int argc, char *argv[]) {
|
|
|
1848
2282
|
return 1;
|
|
1849
2283
|
}
|
|
1850
2284
|
|
|
2285
|
+
if (options.xhtml && options.strict_xhtml) {
|
|
2286
|
+
fprintf(stderr, "Error: --xhtml and --strict-xhtml cannot be used together (use --strict-xhtml alone).\n");
|
|
2287
|
+
return 1;
|
|
2288
|
+
}
|
|
2289
|
+
|
|
1851
2290
|
/* --combine and --mmd-merge are mutually exclusive */
|
|
1852
2291
|
if (combine_mode && mmd_merge_mode) {
|
|
1853
2292
|
fprintf(stderr, "Error: --combine and --mmd-merge cannot be used together\n");
|
|
1854
2293
|
return 1;
|
|
1855
2294
|
}
|
|
1856
2295
|
|
|
2296
|
+
/* Handle theme listing before normal conversion */
|
|
2297
|
+
if (list_themes) {
|
|
2298
|
+
apex_cli_print_highlight_themes();
|
|
2299
|
+
return 0;
|
|
2300
|
+
}
|
|
2301
|
+
|
|
1857
2302
|
/* Handle plugin listing/installation/uninstallation commands before normal conversion */
|
|
1858
2303
|
if (list_plugins || install_plugin_id || uninstall_plugin_id) {
|
|
1859
2304
|
if ((install_plugin_id && uninstall_plugin_id) || (install_plugin_id && list_plugins && uninstall_plugin_id)) {
|
|
@@ -2037,7 +2482,7 @@ int main(int argc, char *argv[]) {
|
|
|
2037
2482
|
cli_free_installed_plugins(installed_head);
|
|
2038
2483
|
return 1;
|
|
2039
2484
|
}
|
|
2040
|
-
apex_remote_print_plugins_filtered(plist, (const char **)installed_ids, installed_count);
|
|
2485
|
+
apex_remote_print_plugins_filtered(plist, (const char **)installed_ids, installed_count, "plugins");
|
|
2041
2486
|
apex_remote_free_plugins(plist);
|
|
2042
2487
|
if (installed_ids) {
|
|
2043
2488
|
for (size_t i = 0; i < installed_count; i++) free(installed_ids[i]);
|
|
@@ -2260,6 +2705,378 @@ int main(int argc, char *argv[]) {
|
|
|
2260
2705
|
}
|
|
2261
2706
|
}
|
|
2262
2707
|
|
|
2708
|
+
/* Handle filter listing/installation/uninstallation before normal conversion.
|
|
2709
|
+
* Filters are distributed from the apex-filters directory:
|
|
2710
|
+
* https://github.com/ApexMarkdown/apex-filters
|
|
2711
|
+
* and installed into:
|
|
2712
|
+
* $XDG_CONFIG_HOME/apex/filters or ~/.config/apex/filters
|
|
2713
|
+
*/
|
|
2714
|
+
if (list_filters || install_filter_id || uninstall_filter_id) {
|
|
2715
|
+
if (install_filter_id && uninstall_filter_id) {
|
|
2716
|
+
fprintf(stderr, "Error: --install-filter and --uninstall-filter cannot be combined.\n");
|
|
2717
|
+
return 1;
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
/* Determine filters root: $XDG_CONFIG_HOME/apex/filters or ~/.config/apex/filters */
|
|
2721
|
+
const char *xdg = getenv("XDG_CONFIG_HOME");
|
|
2722
|
+
char root[1024];
|
|
2723
|
+
if (xdg && *xdg) {
|
|
2724
|
+
snprintf(root, sizeof(root), "%s/apex/filters", xdg);
|
|
2725
|
+
} else {
|
|
2726
|
+
const char *home = getenv("HOME");
|
|
2727
|
+
if (!home || !*home) {
|
|
2728
|
+
fprintf(stderr, "Error: HOME not set; cannot determine filter install directory.\n");
|
|
2729
|
+
return 1;
|
|
2730
|
+
}
|
|
2731
|
+
snprintf(root, sizeof(root), "%s/.config/apex/filters", home);
|
|
2732
|
+
}
|
|
2733
|
+
|
|
2734
|
+
/* Uninstall filter */
|
|
2735
|
+
if (uninstall_filter_id) {
|
|
2736
|
+
char target[1200];
|
|
2737
|
+
snprintf(target, sizeof(target), "%s/%s", root, uninstall_filter_id);
|
|
2738
|
+
|
|
2739
|
+
struct stat st;
|
|
2740
|
+
if (stat(target, &st) != 0) {
|
|
2741
|
+
/* Try with common extensions (e.g. code-includes.lua from path install) */
|
|
2742
|
+
const char *exts[] = { ".lua", ".py", ".rb" };
|
|
2743
|
+
int found = 0;
|
|
2744
|
+
for (size_t e = 0; e < sizeof(exts)/sizeof(exts[0]); e++) {
|
|
2745
|
+
snprintf(target, sizeof(target), "%s/%s%s", root, uninstall_filter_id, exts[e]);
|
|
2746
|
+
if (stat(target, &st) == 0) {
|
|
2747
|
+
found = 1;
|
|
2748
|
+
break;
|
|
2749
|
+
}
|
|
2750
|
+
}
|
|
2751
|
+
if (!found) {
|
|
2752
|
+
snprintf(target, sizeof(target), "%s/%s", root, uninstall_filter_id);
|
|
2753
|
+
fprintf(stderr, "Error: filter '%s' is not installed at %s\n", uninstall_filter_id, target);
|
|
2754
|
+
return 1;
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
|
|
2758
|
+
fprintf(stderr, "About to remove filter:\n %s\n", target);
|
|
2759
|
+
fprintf(stderr, "Proceed? [y/N]: ");
|
|
2760
|
+
fflush(stderr);
|
|
2761
|
+
|
|
2762
|
+
char answer[16];
|
|
2763
|
+
if (!fgets(answer, sizeof(answer), stdin)) {
|
|
2764
|
+
fprintf(stderr, "Aborted.\n");
|
|
2765
|
+
return 1;
|
|
2766
|
+
}
|
|
2767
|
+
if (answer[0] != 'y' && answer[0] != 'Y') {
|
|
2768
|
+
fprintf(stderr, "Aborted.\n");
|
|
2769
|
+
return 1;
|
|
2770
|
+
}
|
|
2771
|
+
|
|
2772
|
+
if (S_ISDIR(st.st_mode)) {
|
|
2773
|
+
char rm_cmd[1400];
|
|
2774
|
+
snprintf(rm_cmd, sizeof(rm_cmd), "rm -rf \"%s\"", target);
|
|
2775
|
+
int rm_rc = system(rm_cmd);
|
|
2776
|
+
if (rm_rc != 0) {
|
|
2777
|
+
fprintf(stderr, "Error: failed to remove filter directory '%s'.\n", target);
|
|
2778
|
+
return 1;
|
|
2779
|
+
}
|
|
2780
|
+
} else {
|
|
2781
|
+
if (unlink(target) != 0) {
|
|
2782
|
+
fprintf(stderr, "Error: failed to remove filter '%s'.\n", target);
|
|
2783
|
+
return 1;
|
|
2784
|
+
}
|
|
2785
|
+
}
|
|
2786
|
+
|
|
2787
|
+
fprintf(stderr, "Uninstalled filter '%s' from %s\n", uninstall_filter_id, target);
|
|
2788
|
+
return 0;
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
/* List filters: installed from root + available from remote directory */
|
|
2792
|
+
if (list_filters) {
|
|
2793
|
+
const char *dir_url = "https://raw.githubusercontent.com/ApexMarkdown/apex-filters/refs/heads/main/apex-filters.json";
|
|
2794
|
+
apex_remote_plugin_list *flist = apex_remote_fetch_filters_directory(dir_url);
|
|
2795
|
+
|
|
2796
|
+
/* Collect installed filter ids (files and directories in root) */
|
|
2797
|
+
char *installed_ids[128];
|
|
2798
|
+
size_t installed_count = 0;
|
|
2799
|
+
DIR *dir = opendir(root);
|
|
2800
|
+
if (dir) {
|
|
2801
|
+
struct dirent *ent;
|
|
2802
|
+
while ((ent = readdir(dir)) != NULL && installed_count < 128) {
|
|
2803
|
+
if (ent->d_name[0] == '.' && (ent->d_name[1] == '\0' || (ent->d_name[1] == '.' && ent->d_name[2] == '\0')))
|
|
2804
|
+
continue;
|
|
2805
|
+
if (strncmp(ent->d_name, ".apex_", 6) == 0)
|
|
2806
|
+
continue;
|
|
2807
|
+
installed_ids[installed_count] = strdup(ent->d_name);
|
|
2808
|
+
if (installed_ids[installed_count])
|
|
2809
|
+
installed_count++;
|
|
2810
|
+
}
|
|
2811
|
+
closedir(dir);
|
|
2812
|
+
}
|
|
2813
|
+
|
|
2814
|
+
printf("## Installed Filters\n");
|
|
2815
|
+
if (installed_count == 0) {
|
|
2816
|
+
printf("(none)\n");
|
|
2817
|
+
} else {
|
|
2818
|
+
for (size_t i = 0; i < installed_count; i++)
|
|
2819
|
+
printf("%s\n", installed_ids[i]);
|
|
2820
|
+
}
|
|
2821
|
+
|
|
2822
|
+
printf("---\n");
|
|
2823
|
+
printf("## Available Filters\n");
|
|
2824
|
+
apex_remote_print_plugins_filtered(flist, (const char **)installed_ids, installed_count, "filters");
|
|
2825
|
+
|
|
2826
|
+
for (size_t i = 0; i < installed_count; i++)
|
|
2827
|
+
free(installed_ids[i]);
|
|
2828
|
+
if (flist) apex_remote_free_plugins(flist);
|
|
2829
|
+
return 0;
|
|
2830
|
+
}
|
|
2831
|
+
|
|
2832
|
+
/* Install filter */
|
|
2833
|
+
if (install_filter_id) {
|
|
2834
|
+
/* Check if install_filter_id is a direct URL/shorthand (GitHub repo) */
|
|
2835
|
+
char *normalized_repo = normalize_plugin_repo_url(install_filter_id);
|
|
2836
|
+
const char *repo = NULL;
|
|
2837
|
+
char *final_filter_id = NULL;
|
|
2838
|
+
char *filter_path = NULL; /* optional: single file path inside repo (e.g. "src/code-includes.lua") */
|
|
2839
|
+
|
|
2840
|
+
if (normalized_repo) {
|
|
2841
|
+
repo = normalized_repo;
|
|
2842
|
+
|
|
2843
|
+
fprintf(stderr,
|
|
2844
|
+
"Apex filters execute unverified code. Only install filters from trusted sources.\n"
|
|
2845
|
+
"Continue? (y/n) ");
|
|
2846
|
+
fflush(stderr);
|
|
2847
|
+
char answer[8] = {0};
|
|
2848
|
+
if (!fgets(answer, sizeof(answer), stdin) ||
|
|
2849
|
+
(answer[0] != 'y' && answer[0] != 'Y')) {
|
|
2850
|
+
fprintf(stderr, "Aborted filter install.\n");
|
|
2851
|
+
free(normalized_repo);
|
|
2852
|
+
return 1;
|
|
2853
|
+
}
|
|
2854
|
+
} else {
|
|
2855
|
+
/* Not a URL - look up in the remote filters directory */
|
|
2856
|
+
const char *dir_url = "https://raw.githubusercontent.com/ApexMarkdown/apex-filters/refs/heads/main/apex-filters.json";
|
|
2857
|
+
char *json = apex_remote_fetch_json(dir_url);
|
|
2858
|
+
if (!json) {
|
|
2859
|
+
fprintf(stderr, "Error: failed to fetch filter directory from %s\n", dir_url);
|
|
2860
|
+
return 1;
|
|
2861
|
+
}
|
|
2862
|
+
|
|
2863
|
+
/* Very small JSON scan: find entry with matching "id" and extract "repo" */
|
|
2864
|
+
const char *p = json;
|
|
2865
|
+
int found = 0;
|
|
2866
|
+
while ((p = strstr(p, "\"id\"")) != NULL) {
|
|
2867
|
+
const char *id_start = strchr(p, ':');
|
|
2868
|
+
if (!id_start) break;
|
|
2869
|
+
id_start++;
|
|
2870
|
+
while (*id_start == ' ' || *id_start == '\t') id_start++;
|
|
2871
|
+
if (*id_start != '\"') { p = id_start; continue; }
|
|
2872
|
+
id_start++;
|
|
2873
|
+
const char *id_end = strchr(id_start, '\"');
|
|
2874
|
+
if (!id_end) break;
|
|
2875
|
+
size_t id_len = (size_t)(id_end - id_start);
|
|
2876
|
+
|
|
2877
|
+
if (strlen(install_filter_id) == id_len &&
|
|
2878
|
+
strncmp(install_filter_id, id_start, id_len) == 0) {
|
|
2879
|
+
/* Found matching id; search for "repo" and optional "path" in this object */
|
|
2880
|
+
const char *obj_start = p;
|
|
2881
|
+
char *repo_val = apex_remote_extract_string(obj_start, "repo");
|
|
2882
|
+
if (!repo_val) {
|
|
2883
|
+
fprintf(stderr, "Error: filter '%s' missing repo URL in directory.\n", install_filter_id);
|
|
2884
|
+
free(json);
|
|
2885
|
+
return 1;
|
|
2886
|
+
}
|
|
2887
|
+
repo = repo_val;
|
|
2888
|
+
final_filter_id = strdup(install_filter_id);
|
|
2889
|
+
filter_path = apex_remote_extract_string(obj_start, "path");
|
|
2890
|
+
found = 1;
|
|
2891
|
+
break;
|
|
2892
|
+
}
|
|
2893
|
+
p = id_end;
|
|
2894
|
+
}
|
|
2895
|
+
|
|
2896
|
+
if (!found) {
|
|
2897
|
+
fprintf(stderr, "Error: filter '%s' not found in directory.\n", install_filter_id);
|
|
2898
|
+
free(json);
|
|
2899
|
+
return 1;
|
|
2900
|
+
}
|
|
2901
|
+
free(json);
|
|
2902
|
+
}
|
|
2903
|
+
|
|
2904
|
+
/* Ensure root directory exists */
|
|
2905
|
+
char mkdir_cmd[1200];
|
|
2906
|
+
snprintf(mkdir_cmd, sizeof(mkdir_cmd), "mkdir -p \"%s\"", root);
|
|
2907
|
+
int mkrc = system(mkdir_cmd);
|
|
2908
|
+
if (mkrc != 0) {
|
|
2909
|
+
fprintf(stderr, "Error: failed to create filter directory '%s'.\n", root);
|
|
2910
|
+
if (normalized_repo) free(normalized_repo);
|
|
2911
|
+
if (final_filter_id) free(final_filter_id);
|
|
2912
|
+
if (filter_path) free(filter_path);
|
|
2913
|
+
if (repo && repo != normalized_repo) free((void *)repo);
|
|
2914
|
+
return 1;
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
/* Single-file install: clone to temp, copy path to root/<basename(path)>, remove temp */
|
|
2918
|
+
if (filter_path && final_filter_id) {
|
|
2919
|
+
const char *path_basename = strrchr(filter_path, '/');
|
|
2920
|
+
path_basename = path_basename ? (path_basename + 1) : filter_path;
|
|
2921
|
+
char final_file[1200];
|
|
2922
|
+
snprintf(final_file, sizeof(final_file), "%s/%s", root, path_basename);
|
|
2923
|
+
char test_cmd[1300];
|
|
2924
|
+
snprintf(test_cmd, sizeof(test_cmd), "[ -e \"%s\" ]", final_file);
|
|
2925
|
+
if (system(test_cmd) == 0) {
|
|
2926
|
+
fprintf(stderr, "Error: filter '%s' already exists at %s. Remove it first to reinstall.\n", final_filter_id, final_file);
|
|
2927
|
+
if (normalized_repo) free(normalized_repo);
|
|
2928
|
+
if (final_filter_id) free(final_filter_id);
|
|
2929
|
+
if (filter_path) free(filter_path);
|
|
2930
|
+
if (repo && repo != normalized_repo) free((void *)repo);
|
|
2931
|
+
return 1;
|
|
2932
|
+
}
|
|
2933
|
+
char temp_target[1200];
|
|
2934
|
+
snprintf(temp_target, sizeof(temp_target), "%s/.apex_install_%s", root, final_filter_id);
|
|
2935
|
+
char clone_cmd[2048];
|
|
2936
|
+
snprintf(clone_cmd, sizeof(clone_cmd), "git clone --depth 1 \"%s\" \"%s\"", repo, temp_target);
|
|
2937
|
+
int git_rc = system(clone_cmd);
|
|
2938
|
+
if (git_rc != 0) {
|
|
2939
|
+
fprintf(stderr, "Error: git clone failed for '%s'. Is git installed and the URL correct?\n", repo);
|
|
2940
|
+
if (normalized_repo) free(normalized_repo);
|
|
2941
|
+
if (final_filter_id) free(final_filter_id);
|
|
2942
|
+
if (filter_path) free(filter_path);
|
|
2943
|
+
if (repo && repo != normalized_repo) free((void *)repo);
|
|
2944
|
+
return 1;
|
|
2945
|
+
}
|
|
2946
|
+
char src_file[1800];
|
|
2947
|
+
snprintf(src_file, sizeof(src_file), "%s/%s", temp_target, filter_path);
|
|
2948
|
+
char cp_cmd[3000];
|
|
2949
|
+
snprintf(cp_cmd, sizeof(cp_cmd), "cp \"%s\" \"%s\"", src_file, final_file);
|
|
2950
|
+
if (system(cp_cmd) != 0) {
|
|
2951
|
+
fprintf(stderr, "Error: failed to copy '%s' from repo to %s\n", filter_path, final_file);
|
|
2952
|
+
snprintf(cp_cmd, sizeof(cp_cmd), "rm -rf \"%s\"", temp_target);
|
|
2953
|
+
system(cp_cmd);
|
|
2954
|
+
if (normalized_repo) free(normalized_repo);
|
|
2955
|
+
if (final_filter_id) free(final_filter_id);
|
|
2956
|
+
if (filter_path) free(filter_path);
|
|
2957
|
+
if (repo && repo != normalized_repo) free((void *)repo);
|
|
2958
|
+
return 1;
|
|
2959
|
+
}
|
|
2960
|
+
char chmod_cmd[1300];
|
|
2961
|
+
snprintf(chmod_cmd, sizeof(chmod_cmd), "chmod +x \"%s\"", final_file);
|
|
2962
|
+
system(chmod_cmd);
|
|
2963
|
+
snprintf(cp_cmd, sizeof(cp_cmd), "rm -rf \"%s\"", temp_target);
|
|
2964
|
+
system(cp_cmd);
|
|
2965
|
+
fprintf(stderr, "Installed filter '%s' into %s\n", final_filter_id, final_file);
|
|
2966
|
+
if (normalized_repo) free(normalized_repo);
|
|
2967
|
+
if (final_filter_id) free(final_filter_id);
|
|
2968
|
+
if (filter_path) free(filter_path);
|
|
2969
|
+
if (repo && repo != normalized_repo) free((void *)repo);
|
|
2970
|
+
return 0;
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2973
|
+
/* Determine temporary or final target directory */
|
|
2974
|
+
char temp_target[1200];
|
|
2975
|
+
if (!final_filter_id) {
|
|
2976
|
+
/* Derive a temporary name from repo URL */
|
|
2977
|
+
const char *last_slash = strrchr(repo, '/');
|
|
2978
|
+
const char *name_start = last_slash ? (last_slash + 1) : repo;
|
|
2979
|
+
const char *name_end = strstr(name_start, ".git");
|
|
2980
|
+
if (!name_end) name_end = name_start + strlen(name_start);
|
|
2981
|
+
size_t name_len = name_end - name_start;
|
|
2982
|
+
if (name_len > 0 && name_len < 200) {
|
|
2983
|
+
char temp_name[256];
|
|
2984
|
+
memcpy(temp_name, name_start, name_len);
|
|
2985
|
+
temp_name[name_len] = '\0';
|
|
2986
|
+
snprintf(temp_target, sizeof(temp_target), "%s/.apex_install_%s", root, temp_name);
|
|
2987
|
+
} else {
|
|
2988
|
+
snprintf(temp_target, sizeof(temp_target), "%s/.apex_install_temp", root);
|
|
2989
|
+
}
|
|
2990
|
+
} else {
|
|
2991
|
+
snprintf(temp_target, sizeof(temp_target), "%s/%s", root, final_filter_id);
|
|
2992
|
+
}
|
|
2993
|
+
|
|
2994
|
+
/* Refuse to overwrite existing directory when using a final id */
|
|
2995
|
+
if (final_filter_id) {
|
|
2996
|
+
char test_cmd[1300];
|
|
2997
|
+
snprintf(test_cmd, sizeof(test_cmd), "[ -d \"%s\" ]", temp_target);
|
|
2998
|
+
int exists_rc = system(test_cmd);
|
|
2999
|
+
if (exists_rc == 0) {
|
|
3000
|
+
fprintf(stderr, "Error: filter directory '%s' already exists. Remove it first to reinstall.\n", temp_target);
|
|
3001
|
+
if (normalized_repo) free(normalized_repo);
|
|
3002
|
+
if (final_filter_id) free(final_filter_id);
|
|
3003
|
+
if (filter_path) free(filter_path);
|
|
3004
|
+
if (repo && repo != normalized_repo) free((void *)repo);
|
|
3005
|
+
return 1;
|
|
3006
|
+
}
|
|
3007
|
+
}
|
|
3008
|
+
|
|
3009
|
+
/* Clone repo using git */
|
|
3010
|
+
char clone_cmd[2048];
|
|
3011
|
+
snprintf(clone_cmd, sizeof(clone_cmd), "git clone \"%s\" \"%s\"", repo, temp_target);
|
|
3012
|
+
int git_rc = system(clone_cmd);
|
|
3013
|
+
if (git_rc != 0) {
|
|
3014
|
+
fprintf(stderr, "Error: git clone failed for '%s'. Is git installed and the URL correct?\n", repo);
|
|
3015
|
+
if (normalized_repo) free(normalized_repo);
|
|
3016
|
+
if (final_filter_id) free(final_filter_id);
|
|
3017
|
+
if (filter_path) free(filter_path);
|
|
3018
|
+
if (repo && repo != normalized_repo) free((void *)repo);
|
|
3019
|
+
return 1;
|
|
3020
|
+
}
|
|
3021
|
+
|
|
3022
|
+
/* If we didn't get a final id from the directory, use install_filter_id as the final name */
|
|
3023
|
+
if (!final_filter_id) {
|
|
3024
|
+
final_filter_id = strdup(install_filter_id);
|
|
3025
|
+
if (!final_filter_id) {
|
|
3026
|
+
fprintf(stderr, "Error: Memory allocation failed\n");
|
|
3027
|
+
if (normalized_repo) free(normalized_repo);
|
|
3028
|
+
if (filter_path) free(filter_path);
|
|
3029
|
+
if (repo && repo != normalized_repo) free((void *)repo);
|
|
3030
|
+
return 1;
|
|
3031
|
+
}
|
|
3032
|
+
}
|
|
3033
|
+
|
|
3034
|
+
char final_target[1200];
|
|
3035
|
+
snprintf(final_target, sizeof(final_target), "%s/%s", root, final_filter_id);
|
|
3036
|
+
|
|
3037
|
+
/* If temp_target != final_target, move into place */
|
|
3038
|
+
if (strcmp(temp_target, final_target) != 0) {
|
|
3039
|
+
char final_test_cmd[1300];
|
|
3040
|
+
snprintf(final_test_cmd, sizeof(final_test_cmd), "[ -d \"%s\" ]", final_target);
|
|
3041
|
+
int final_exists_rc = system(final_test_cmd);
|
|
3042
|
+
if (final_exists_rc == 0) {
|
|
3043
|
+
fprintf(stderr, "Error: filter directory '%s' already exists. Remove it first to reinstall.\n", final_target);
|
|
3044
|
+
char rm_cmd[1300];
|
|
3045
|
+
snprintf(rm_cmd, sizeof(rm_cmd), "rm -rf \"%s\"", temp_target);
|
|
3046
|
+
system(rm_cmd);
|
|
3047
|
+
free(final_filter_id);
|
|
3048
|
+
if (normalized_repo) free(normalized_repo);
|
|
3049
|
+
if (filter_path) free(filter_path);
|
|
3050
|
+
if (repo && repo != normalized_repo) free((void *)repo);
|
|
3051
|
+
return 1;
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
char mv_cmd[2500];
|
|
3055
|
+
snprintf(mv_cmd, sizeof(mv_cmd), "mv \"%s\" \"%s\"", temp_target, final_target);
|
|
3056
|
+
int mv_rc = system(mv_cmd);
|
|
3057
|
+
if (mv_rc != 0) {
|
|
3058
|
+
fprintf(stderr, "Error: failed to move filter to final location '%s'.\n", final_target);
|
|
3059
|
+
char rm_cmd[1300];
|
|
3060
|
+
snprintf(rm_cmd, sizeof(rm_cmd), "rm -rf \"%s\"", temp_target);
|
|
3061
|
+
system(rm_cmd);
|
|
3062
|
+
free(final_filter_id);
|
|
3063
|
+
if (normalized_repo) free(normalized_repo);
|
|
3064
|
+
if (filter_path) free(filter_path);
|
|
3065
|
+
if (repo && repo != normalized_repo) free((void *)repo);
|
|
3066
|
+
return 1;
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
|
|
3070
|
+
fprintf(stderr, "Installed filter '%s' into %s\n", final_filter_id, final_target);
|
|
3071
|
+
|
|
3072
|
+
if (normalized_repo) free(normalized_repo);
|
|
3073
|
+
if (final_filter_id) free(final_filter_id);
|
|
3074
|
+
if (filter_path) free(filter_path);
|
|
3075
|
+
if (repo && repo != normalized_repo) free((void *)repo);
|
|
3076
|
+
return 0;
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
|
|
2263
3080
|
/* mmd-merge mode: emulate MultiMarkdown mmd_merge.pl and exit */
|
|
2264
3081
|
if (mmd_merge_mode) {
|
|
2265
3082
|
FILE *out = stdout;
|
|
@@ -2604,9 +3421,11 @@ int main(int argc, char *argv[]) {
|
|
|
2604
3421
|
|
|
2605
3422
|
/* Apply metadata to options - allows per-document control of command-line options */
|
|
2606
3423
|
/* Note: Bibliography file loading from metadata will be handled in citations extension */
|
|
3424
|
+
apex_output_format_t saved_output_format = options.output_format;
|
|
2607
3425
|
if (merged_metadata) {
|
|
2608
3426
|
apex_apply_metadata_to_options(merged_metadata, &options);
|
|
2609
|
-
/* Restore
|
|
3427
|
+
/* Restore explicit CLI choices that metadata mode reset */
|
|
3428
|
+
options.output_format = saved_output_format;
|
|
2610
3429
|
if (saved_bibliography_files && !options.bibliography_files) {
|
|
2611
3430
|
options.bibliography_files = saved_bibliography_files;
|
|
2612
3431
|
}
|
|
@@ -2622,6 +3441,232 @@ int main(int argc, char *argv[]) {
|
|
|
2622
3441
|
options.enable_plugins = plugins_cli_value;
|
|
2623
3442
|
}
|
|
2624
3443
|
|
|
3444
|
+
/* Resolve AST filters configured from CLI into absolute command paths.
|
|
3445
|
+
* Filters live in $XDG_CONFIG_HOME/apex/filters or ~/.config/apex/filters.
|
|
3446
|
+
*/
|
|
3447
|
+
char **ast_filter_commands = NULL;
|
|
3448
|
+
size_t ast_filter_count = 0;
|
|
3449
|
+
if (run_all_filters_dir || ast_filter_name_count > 0 || lua_filter_count > 0) {
|
|
3450
|
+
const char *xdg = getenv("XDG_CONFIG_HOME");
|
|
3451
|
+
char root[1024];
|
|
3452
|
+
if (xdg && *xdg) {
|
|
3453
|
+
snprintf(root, sizeof(root), "%s/apex/filters", xdg);
|
|
3454
|
+
} else {
|
|
3455
|
+
const char *home = getenv("HOME");
|
|
3456
|
+
if (!home || !*home) {
|
|
3457
|
+
fprintf(stderr, "Error: HOME not set; cannot determine filters directory.\n");
|
|
3458
|
+
return 1;
|
|
3459
|
+
}
|
|
3460
|
+
snprintf(root, sizeof(root), "%s/.config/apex/filters", home);
|
|
3461
|
+
}
|
|
3462
|
+
|
|
3463
|
+
/* Ensure root directory exists when running --filters or --filter */
|
|
3464
|
+
struct stat st_root;
|
|
3465
|
+
if (stat(root, &st_root) != 0 || !S_ISDIR(st_root.st_mode)) {
|
|
3466
|
+
if (run_all_filters_dir || ast_filter_name_count > 0) {
|
|
3467
|
+
fprintf(stderr, "Error: filters directory '%s' does not exist.\n", root);
|
|
3468
|
+
return 1;
|
|
3469
|
+
}
|
|
3470
|
+
}
|
|
3471
|
+
|
|
3472
|
+
/* Collect all filters from directory when --filters is set */
|
|
3473
|
+
if (run_all_filters_dir) {
|
|
3474
|
+
DIR *d = opendir(root);
|
|
3475
|
+
if (!d) {
|
|
3476
|
+
fprintf(stderr, "Error: cannot open filters directory '%s'\n", root);
|
|
3477
|
+
return 1;
|
|
3478
|
+
}
|
|
3479
|
+
struct dirent *ent;
|
|
3480
|
+
size_t capacity = 8;
|
|
3481
|
+
ast_filter_commands = malloc(capacity * sizeof(char *));
|
|
3482
|
+
if (!ast_filter_commands) {
|
|
3483
|
+
closedir(d);
|
|
3484
|
+
fprintf(stderr, "Error: Memory allocation failed\n");
|
|
3485
|
+
return 1;
|
|
3486
|
+
}
|
|
3487
|
+
while ((ent = readdir(d)) != NULL) {
|
|
3488
|
+
if (ent->d_name[0] == '.') continue;
|
|
3489
|
+
char path[1200];
|
|
3490
|
+
snprintf(path, sizeof(path), "%s/%s", root, ent->d_name);
|
|
3491
|
+
struct stat st;
|
|
3492
|
+
if (stat(path, &st) != 0 || !S_ISREG(st.st_mode)) {
|
|
3493
|
+
continue;
|
|
3494
|
+
}
|
|
3495
|
+
if (ast_filter_count == capacity) {
|
|
3496
|
+
capacity *= 2;
|
|
3497
|
+
char **tmp = realloc(ast_filter_commands, capacity * sizeof(char *));
|
|
3498
|
+
if (!tmp) {
|
|
3499
|
+
closedir(d);
|
|
3500
|
+
fprintf(stderr, "Error: Memory allocation failed\n");
|
|
3501
|
+
return 1;
|
|
3502
|
+
}
|
|
3503
|
+
ast_filter_commands = tmp;
|
|
3504
|
+
}
|
|
3505
|
+
ast_filter_commands[ast_filter_count] = strdup(path);
|
|
3506
|
+
if (!ast_filter_commands[ast_filter_count]) {
|
|
3507
|
+
closedir(d);
|
|
3508
|
+
fprintf(stderr, "Error: Memory allocation failed\n");
|
|
3509
|
+
return 1;
|
|
3510
|
+
}
|
|
3511
|
+
ast_filter_count++;
|
|
3512
|
+
}
|
|
3513
|
+
closedir(d);
|
|
3514
|
+
}
|
|
3515
|
+
|
|
3516
|
+
/* Resolve individual --filter NAME entries to absolute paths.
|
|
3517
|
+
*
|
|
3518
|
+
* Resolution rules:
|
|
3519
|
+
* - If root/NAME is a regular file, use that directly.
|
|
3520
|
+
* - If root/NAME is a directory, look for a script inside it:
|
|
3521
|
+
* root/NAME/NAME
|
|
3522
|
+
* root/NAME/NAME.lua
|
|
3523
|
+
* root/NAME/NAME.py
|
|
3524
|
+
* root/NAME/NAME.rb
|
|
3525
|
+
* and use the first regular file found.
|
|
3526
|
+
*/
|
|
3527
|
+
if (ast_filter_name_count > 0) {
|
|
3528
|
+
size_t capacity = (ast_filter_commands ? ast_filter_count : 0) + ast_filter_name_count;
|
|
3529
|
+
if (!ast_filter_commands) {
|
|
3530
|
+
ast_filter_commands = malloc(capacity * sizeof(char *));
|
|
3531
|
+
if (!ast_filter_commands) {
|
|
3532
|
+
fprintf(stderr, "Error: Memory allocation failed\n");
|
|
3533
|
+
return 1;
|
|
3534
|
+
}
|
|
3535
|
+
} else {
|
|
3536
|
+
char **tmp = realloc(ast_filter_commands, capacity * sizeof(char *));
|
|
3537
|
+
if (!tmp) {
|
|
3538
|
+
fprintf(stderr, "Error: Memory allocation failed\n");
|
|
3539
|
+
return 1;
|
|
3540
|
+
}
|
|
3541
|
+
ast_filter_commands = tmp;
|
|
3542
|
+
}
|
|
3543
|
+
|
|
3544
|
+
for (size_t i = 0; i < ast_filter_name_count; i++) {
|
|
3545
|
+
const char *name = ast_filter_names[i];
|
|
3546
|
+
char path[1200];
|
|
3547
|
+
snprintf(path, sizeof(path), "%s/%s", root, name);
|
|
3548
|
+
struct stat st;
|
|
3549
|
+
int path_exists = (stat(path, &st) == 0);
|
|
3550
|
+
|
|
3551
|
+
char resolved[1400];
|
|
3552
|
+
int resolved_ok = 0;
|
|
3553
|
+
|
|
3554
|
+
/* Try root/NAME as regular file first */
|
|
3555
|
+
if (path_exists && S_ISREG(st.st_mode)) {
|
|
3556
|
+
snprintf(resolved, sizeof(resolved), "%s", path);
|
|
3557
|
+
resolved_ok = 1;
|
|
3558
|
+
}
|
|
3559
|
+
/* Else try root/NAME.lua, root/NAME.py, root/NAME.rb as regular files (single-file installs) */
|
|
3560
|
+
if (!resolved_ok) {
|
|
3561
|
+
const char *exts[] = { ".lua", ".py", ".rb" };
|
|
3562
|
+
for (size_t e = 0; e < sizeof(exts)/sizeof(exts[0]); e++) {
|
|
3563
|
+
char with_ext[1400];
|
|
3564
|
+
snprintf(with_ext, sizeof(with_ext), "%s/%s%s", root, name, exts[e]);
|
|
3565
|
+
struct stat ste;
|
|
3566
|
+
if (stat(with_ext, &ste) == 0 && S_ISREG(ste.st_mode)) {
|
|
3567
|
+
snprintf(resolved, sizeof(resolved), "%s", with_ext);
|
|
3568
|
+
resolved_ok = 1;
|
|
3569
|
+
break;
|
|
3570
|
+
}
|
|
3571
|
+
}
|
|
3572
|
+
}
|
|
3573
|
+
/* Else if root/NAME is a directory, look for script inside */
|
|
3574
|
+
if (!resolved_ok && path_exists && S_ISDIR(st.st_mode)) {
|
|
3575
|
+
const char *candidates[4];
|
|
3576
|
+
char buf0[1400], buf1[1400], buf2[1400], buf3[1400];
|
|
3577
|
+
|
|
3578
|
+
snprintf(buf0, sizeof(buf0), "%s/%s", path, name);
|
|
3579
|
+
snprintf(buf1, sizeof(buf1), "%s/%s.lua", path, name);
|
|
3580
|
+
snprintf(buf2, sizeof(buf2), "%s/%s.py", path, name);
|
|
3581
|
+
snprintf(buf3, sizeof(buf3), "%s/%s.rb", path, name);
|
|
3582
|
+
|
|
3583
|
+
candidates[0] = buf0;
|
|
3584
|
+
candidates[1] = buf1;
|
|
3585
|
+
candidates[2] = buf2;
|
|
3586
|
+
candidates[3] = buf3;
|
|
3587
|
+
|
|
3588
|
+
for (size_t c = 0; c < 4; c++) {
|
|
3589
|
+
struct stat stc;
|
|
3590
|
+
if (stat(candidates[c], &stc) == 0 && S_ISREG(stc.st_mode)) {
|
|
3591
|
+
snprintf(resolved, sizeof(resolved), "%s", candidates[c]);
|
|
3592
|
+
resolved_ok = 1;
|
|
3593
|
+
break;
|
|
3594
|
+
}
|
|
3595
|
+
}
|
|
3596
|
+
|
|
3597
|
+
if (!resolved_ok) {
|
|
3598
|
+
fprintf(stderr,
|
|
3599
|
+
"Error: filter '%s' is a directory at %s but no executable script was found inside.\n",
|
|
3600
|
+
name, path);
|
|
3601
|
+
fprintf(stderr,
|
|
3602
|
+
"Tried: %s/%s, %s/%s.lua, %s/%s.py, %s/%s.rb\n",
|
|
3603
|
+
path, name, path, name, path, name, path, name);
|
|
3604
|
+
return 1;
|
|
3605
|
+
}
|
|
3606
|
+
}
|
|
3607
|
+
if (!resolved_ok) {
|
|
3608
|
+
fprintf(stderr, "Error: filter '%s' not found at %s (or %s.lua, %s.py, %s.rb)\n",
|
|
3609
|
+
name, path, path, path, path);
|
|
3610
|
+
return 1;
|
|
3611
|
+
}
|
|
3612
|
+
|
|
3613
|
+
/* Lua scripts without shebang: run via `lua "path"` */
|
|
3614
|
+
size_t rlen = strlen(resolved);
|
|
3615
|
+
int is_lua = (rlen > 4 && strcmp(resolved + rlen - 4, ".lua") == 0);
|
|
3616
|
+
if (is_lua) {
|
|
3617
|
+
char cmd[1400];
|
|
3618
|
+
snprintf(cmd, sizeof(cmd), "lua \"%s\"", resolved);
|
|
3619
|
+
ast_filter_commands[ast_filter_count] = strdup(cmd);
|
|
3620
|
+
} else {
|
|
3621
|
+
ast_filter_commands[ast_filter_count] = strdup(resolved);
|
|
3622
|
+
}
|
|
3623
|
+
if (!ast_filter_commands[ast_filter_count]) {
|
|
3624
|
+
fprintf(stderr, "Error: Memory allocation failed\n");
|
|
3625
|
+
return 1;
|
|
3626
|
+
}
|
|
3627
|
+
ast_filter_count++;
|
|
3628
|
+
}
|
|
3629
|
+
}
|
|
3630
|
+
|
|
3631
|
+
/* Append explicit Lua filters as commands: `lua <script>` */
|
|
3632
|
+
if (lua_filter_count > 0) {
|
|
3633
|
+
size_t capacity = (ast_filter_commands ? ast_filter_count : 0) + lua_filter_count;
|
|
3634
|
+
if (!ast_filter_commands) {
|
|
3635
|
+
ast_filter_commands = malloc(capacity * sizeof(char *));
|
|
3636
|
+
if (!ast_filter_commands) {
|
|
3637
|
+
fprintf(stderr, "Error: Memory allocation failed\n");
|
|
3638
|
+
return 1;
|
|
3639
|
+
}
|
|
3640
|
+
} else {
|
|
3641
|
+
char **tmp = realloc(ast_filter_commands, capacity * sizeof(char *));
|
|
3642
|
+
if (!tmp) {
|
|
3643
|
+
fprintf(stderr, "Error: Memory allocation failed\n");
|
|
3644
|
+
return 1;
|
|
3645
|
+
}
|
|
3646
|
+
ast_filter_commands = tmp;
|
|
3647
|
+
}
|
|
3648
|
+
|
|
3649
|
+
for (size_t i = 0; i < lua_filter_count; i++) {
|
|
3650
|
+
const char *script = lua_filter_paths[i];
|
|
3651
|
+
/* Build a simple 'lua "<script>"' command */
|
|
3652
|
+
char cmd[1400];
|
|
3653
|
+
snprintf(cmd, sizeof(cmd), "lua \"%s\"", script);
|
|
3654
|
+
ast_filter_commands[ast_filter_count] = strdup(cmd);
|
|
3655
|
+
if (!ast_filter_commands[ast_filter_count]) {
|
|
3656
|
+
fprintf(stderr, "Error: Memory allocation failed\n");
|
|
3657
|
+
return 1;
|
|
3658
|
+
}
|
|
3659
|
+
ast_filter_count++;
|
|
3660
|
+
}
|
|
3661
|
+
}
|
|
3662
|
+
|
|
3663
|
+
if (ast_filter_count > 0) {
|
|
3664
|
+
options.ast_filter_commands = (const char **)ast_filter_commands;
|
|
3665
|
+
options.ast_filter_count = ast_filter_count;
|
|
3666
|
+
options.ast_filter_strict = ast_filters_strict;
|
|
3667
|
+
}
|
|
3668
|
+
}
|
|
3669
|
+
|
|
2625
3670
|
/* Attach any collected script tags to options as a NULL-terminated array */
|
|
2626
3671
|
if (script_tags) {
|
|
2627
3672
|
/* Ensure NULL terminator */
|
|
@@ -2648,7 +3693,12 @@ int main(int argc, char *argv[]) {
|
|
|
2648
3693
|
last_stage = NULL;
|
|
2649
3694
|
}
|
|
2650
3695
|
|
|
2651
|
-
/*
|
|
3696
|
+
/* Man page output must keep -- as literal double hyphen; option names must not become en-dash */
|
|
3697
|
+
if (options.output_format == APEX_OUTPUT_MAN || options.output_format == APEX_OUTPUT_MAN_HTML) {
|
|
3698
|
+
options.enable_smart_typography = false;
|
|
3699
|
+
}
|
|
3700
|
+
|
|
3701
|
+
/* Convert to output (HTML, Markdown, terminal, etc.) */
|
|
2652
3702
|
char *html = apex_markdown_to_html(final_markdown, final_len, &options);
|
|
2653
3703
|
|
|
2654
3704
|
/* Check if we should show delayed progress (in case processing took > 1s but no progress was shown) */
|
|
@@ -2675,9 +3725,61 @@ int main(int argc, char *argv[]) {
|
|
|
2675
3725
|
return 1;
|
|
2676
3726
|
}
|
|
2677
3727
|
|
|
2678
|
-
/*
|
|
3728
|
+
/* For terminal output, optionally wrap to a fixed width when requested.
|
|
3729
|
+
* Precedence: CLI --width > metadata/config terminal.width > theme default.
|
|
3730
|
+
* (Theme files are currently not consulted for width.)
|
|
3731
|
+
*/
|
|
3732
|
+
if (options.output_format == APEX_OUTPUT_TERMINAL ||
|
|
3733
|
+
options.output_format == APEX_OUTPUT_TERMINAL256) {
|
|
3734
|
+
int effective_width = 0;
|
|
3735
|
+
if (width_override > 0) {
|
|
3736
|
+
effective_width = width_override;
|
|
3737
|
+
} else if (options.terminal_width > 0) {
|
|
3738
|
+
effective_width = options.terminal_width;
|
|
3739
|
+
}
|
|
3740
|
+
if (effective_width > 0) {
|
|
3741
|
+
char *wrapped = wrap_ansi_to_width(html, effective_width);
|
|
3742
|
+
if (wrapped) {
|
|
3743
|
+
apex_free_string(html);
|
|
3744
|
+
html = wrapped;
|
|
3745
|
+
}
|
|
3746
|
+
}
|
|
3747
|
+
}
|
|
3748
|
+
|
|
3749
|
+
/* Determine whether to paginate terminal output.
|
|
3750
|
+
* Pagination is only applied for terminal/terminal256 output when writing to stdout.
|
|
3751
|
+
* Precedence: CLI -p/--paginate OR config/metadata paginate:true.
|
|
3752
|
+
*/
|
|
3753
|
+
bool paginate_effective = false;
|
|
3754
|
+
if (!output_file &&
|
|
3755
|
+
(options.output_format == APEX_OUTPUT_TERMINAL ||
|
|
3756
|
+
options.output_format == APEX_OUTPUT_TERMINAL256)) {
|
|
3757
|
+
if (paginate_cli || options.paginate) {
|
|
3758
|
+
paginate_effective = true;
|
|
3759
|
+
}
|
|
3760
|
+
}
|
|
3761
|
+
|
|
3762
|
+
/* Write output (optionally via pager) */
|
|
2679
3763
|
PROFILE_START(file_write);
|
|
2680
|
-
if (
|
|
3764
|
+
if (paginate_effective) {
|
|
3765
|
+
const char *pager_cmd = getenv("APEX_PAGER");
|
|
3766
|
+
if (!pager_cmd || !*pager_cmd) {
|
|
3767
|
+
pager_cmd = getenv("PAGER");
|
|
3768
|
+
}
|
|
3769
|
+
if (!pager_cmd || !*pager_cmd) {
|
|
3770
|
+
pager_cmd = "less -R";
|
|
3771
|
+
}
|
|
3772
|
+
|
|
3773
|
+
FILE *pager = popen(pager_cmd, "w");
|
|
3774
|
+
size_t html_len = strlen(html);
|
|
3775
|
+
if (!pager) {
|
|
3776
|
+
/* Fall back to direct stdout if pager cannot be started */
|
|
3777
|
+
fwrite(html, 1, html_len, stdout);
|
|
3778
|
+
} else {
|
|
3779
|
+
fwrite(html, 1, html_len, pager);
|
|
3780
|
+
pclose(pager);
|
|
3781
|
+
}
|
|
3782
|
+
} else if (output_file) {
|
|
2681
3783
|
FILE *fp = fopen(output_file, "w");
|
|
2682
3784
|
if (!fp) {
|
|
2683
3785
|
fprintf(stderr, "Error: Cannot open output file '%s'\n", output_file);
|
|
@@ -2711,6 +3813,16 @@ int main(int argc, char *argv[]) {
|
|
|
2711
3813
|
free(script_tags);
|
|
2712
3814
|
}
|
|
2713
3815
|
|
|
3816
|
+
/* Free AST filter command paths allocated for this run */
|
|
3817
|
+
if (ast_filter_commands) {
|
|
3818
|
+
for (size_t i = 0; i < ast_filter_count; i++) {
|
|
3819
|
+
free(ast_filter_commands[i]);
|
|
3820
|
+
}
|
|
3821
|
+
free(ast_filter_commands);
|
|
3822
|
+
}
|
|
3823
|
+
|
|
3824
|
+
/* lua_filter_paths entries are argv pointers; no need to free them here */
|
|
3825
|
+
|
|
2714
3826
|
/* Free base_directory if we allocated it */
|
|
2715
3827
|
if (allocated_base_dir) {
|
|
2716
3828
|
free(allocated_base_dir);
|