diff --git a/CMakeLists.txt b/CMakeLists.txt index c00ae51..4a05726 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,5 +1,5 @@ cmake_minimum_required(VERSION 3.10) -project(fun VERSION 0.41.8 LANGUAGES C) +project(fun VERSION 0.41.9 LANGUAGES C) set(CMAKE_C_STANDARD 99) set(CMAKE_C_STANDARD_REQUIRED ON) @@ -430,6 +430,26 @@ if(BUILD_TESTING) else() message(WARNING "include_line_mapping_test.fun not found; skipping include_line_mapping CTest") endif() + + # KCGI example smoke test (only when the KCGI extension is enabled) + # We run the CGI example with minimal environment to avoid RFC warnings + # and assert the body contains the expected greeting. + if(FUN_WITH_KCGI) + set(_kcgi_example "${CMAKE_SOURCE_DIR}/examples/cgi/hello_kcgi.fun") + if(EXISTS "${_kcgi_example}") + add_test(NAME kcgi_hello + COMMAND $ "${_kcgi_example}" + WORKING_DIRECTORY ${CMAKE_BINARY_DIR} + ) + # Provide standard library path and minimal CGI environment + set_tests_properties(kcgi_hello PROPERTIES + ENVIRONMENT "FUN_LIB_DIR=${CMAKE_SOURCE_DIR}/lib;REQUEST_METHOD=GET;QUERY_STRING=name=Fun;SERVER_NAME=localhost;SERVER_PORT=80;SCRIPT_NAME=/hello_kcgi.fun;REMOTE_ADDR=127.0.0.1" + PASS_REGULAR_EXPRESSION "Hello, Fun!" + ) + else() + message(WARNING "KCGI example not found: ${_kcgi_example}; skipping kcgi_hello CTest") + endif() + endif() endif() # --- Doxygen docs target (optional) --- diff --git a/README.md b/README.md index 55ab42d..3690522 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ See [./lib/](https://git.xw3.org/fun/fun/src/branch/main/lib) for what the stand ### Optional extensions (build-time selectable / only testing this on Linux actually): -- [CGI](https://en.wikipedia.org/wiki/Common_Gateway_Interface) support builtin using [kcgi](https://kristaps.bsd.lv/kcgi/) (optional) ☐ +- [CGI](https://en.wikipedia.org/wiki/Common_Gateway_Interface) support builtin using [kcgi](https://kristaps.bsd.lv/kcgi/) (optional) — see [docs](./web/documentation/extensions/kcgi/kcgi.md) ☑ - [cURL (libcurl)](./web/documentation/extensions/curl/curl.md) (optional) ☑ - [INI (iniparser)](./web/documentation/extensions/ini/ini.md) (optional) ☑ - [JSON (json-c)](./web/documentation/extensions/json/json.md) (optional) ☑ diff --git a/cmake/Extensions/Extensions.cmake b/cmake/Extensions/Extensions.cmake index 65c04d9..215a99b 100644 --- a/cmake/Extensions/Extensions.cmake +++ b/cmake/Extensions/Extensions.cmake @@ -20,17 +20,18 @@ include(${CMAKE_SOURCE_DIR}/cmake/Extensions/PCRE2.cmake) include(${CMAKE_SOURCE_DIR}/cmake/Extensions/CURL.cmake) include(${CMAKE_SOURCE_DIR}/cmake/Extensions/REPL.cmake) include(${CMAKE_SOURCE_DIR}/cmake/Extensions/OPENSSL.cmake) +include(${CMAKE_SOURCE_DIR}/cmake/Extensions/KCGI.cmake) # Summary of extension toggles message(STATUS "---- Fun extension summary ----") _fun_print_feature("SQLite (FUN_WITH_SQLITE)" FUN_WITH_SQLITE) _fun_print_feature("PCSC (FUN_WITH_PCSC)" FUN_WITH_PCSC) -_fun_print_feature("JSON-C (FUN_WITH_JSON)" FUN_WITH_JSON) +_fun_print_feature("json-c (FUN_WITH_JSON)" FUN_WITH_JSON) _fun_print_feature("INI/iniparser (FUN_WITH_INI)" FUN_WITH_INI) _fun_print_feature("libxml2 (FUN_WITH_XML2)" FUN_WITH_XML2) -## libsql extension removed _fun_print_feature("PCRE2 (FUN_WITH_PCRE2)" FUN_WITH_PCRE2) -_fun_print_feature("libcurl (FUN_WITH_CURL)" FUN_WITH_CURL) +_fun_print_feature("curl (FUN_WITH_CURL)" FUN_WITH_CURL) _fun_print_feature("REPL (FUN_WITH_REPL)" FUN_WITH_REPL) _fun_print_feature("OpenSSL (FUN_WITH_OPENSSL)" FUN_WITH_OPENSSL) +_fun_print_feature("kcgi (FUN_WITH_KCGI)" FUN_WITH_KCGI) message(STATUS "--------------------------------") diff --git a/cmake/Extensions/KCGI.cmake b/cmake/Extensions/KCGI.cmake new file mode 100644 index 0000000..4276634 --- /dev/null +++ b/cmake/Extensions/KCGI.cmake @@ -0,0 +1,19 @@ +# KCGI (kcgi) +option(FUN_WITH_KCGI "Enable kcgi support (CGI/FastCGI via kcgi)" OFF) +set(KCGI_INCLUDE_DIRS "") +set(KCGI_LINK_LIBS "") + +if(FUN_WITH_KCGI) + add_definitions(-DFUN_WITH_KCGI) + find_package(PkgConfig QUIET) + if(PKG_CONFIG_FOUND) + pkg_check_modules(KCGI QUIET kcgi) + endif() + if(KCGI_FOUND) + list(APPEND KCGI_INCLUDE_DIRS ${KCGI_INCLUDE_DIRS} ${KCGI_INCLUDE_DIRS}) + list(APPEND KCGI_LINK_LIBS ${KCGI_LINK_LIBS} ${KCGI_LIBRARIES}) + else() + # Fallback libs commonly required for kcgi on Linux + list(APPEND KCGI_LINK_LIBS kcgi z) + endif() +endif() diff --git a/cmake/Targets.cmake b/cmake/Targets.cmake index 9deaeb2..8ce3db9 100644 --- a/cmake/Targets.cmake +++ b/cmake/Targets.cmake @@ -42,7 +42,8 @@ foreach(var_pair INIPARSER LIBSQL LIBXML2 - OPENSSL) + OPENSSL + KCGI) if(${var_pair}_INCLUDE_DIRS) target_include_directories(fun_core PRIVATE ${${var_pair}_INCLUDE_DIRS}) endif() @@ -93,6 +94,10 @@ if(FUN_WITH_OPENSSL) target_compile_definitions(fun_core PUBLIC FUN_WITH_OPENSSL=1) endif() +if(FUN_WITH_KCGI) + target_compile_definitions(fun_core PUBLIC FUN_WITH_KCGI=1) +endif() + # LibreSSL toggle: ensure compile definitions are applied to fun_core so # the LibreSSL code paths are compiled, and linking vars are handled above. diff --git a/examples/cgi/hello_kcgi.fun b/examples/cgi/hello_kcgi.fun new file mode 100644 index 0000000..6fce745 --- /dev/null +++ b/examples/cgi/hello_kcgi.fun @@ -0,0 +1,14 @@ +#!/usr/bin/env fun + +/* + * Minimal example using KCGI wrapper. + * Run under a CGI/FastCGI environment where kcgi can parse the request. + */ + +#include + +cgi = KCGI() +name = cgi.get_or("name", "Fun") + +body = "

Hello, " + to_string(name) + "!

" +cgi.reply(200, "text/html; charset=utf-8", body) diff --git a/lib/net/kcgi.fun b/lib/net/kcgi.fun new file mode 100644 index 0000000..796306d --- /dev/null +++ b/lib/net/kcgi.fun @@ -0,0 +1,46 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2026 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + */ + +// Thin wrapper around VM kcgi intrinsics + +class KCGI() + fun _construct(this) + this.req = kcgi_parse() + + fun get(this, key) + if (typeof(this.req) == "Map") + fields = this.req["fields"] + if (typeof(fields) == "Map") + return fields[key] + return nil + + // Returns the field value if present and non-nil, otherwise returns 'def'. + fun get_or(this, key, def) + v = this.get(key) + if (v == nil) + return def + // Some code may stringify nil to "nil"; treat that as missing too. + if (typeof(v) == "String" && v == "nil") + return def + return v + + fun reply(this, code, content_type, body) + if (kcgi_reply_start(code, content_type) == 1) + _ = kcgi_write(to_string(body)) + _ = kcgi_end() + + // streaming API + fun start(this, code, content_type) + return kcgi_reply_start(code, content_type) + + fun write(this, chunk) + return kcgi_write(to_string(chunk)) + + fun end(this) + return kcgi_end() diff --git a/make b/make index 8fac7ba..c26a06a 100755 --- a/make +++ b/make @@ -29,6 +29,7 @@ if [ "$target" = "all" ]; then -DFUN_WITH_JSON=ON \ -DFUN_WITH_INI=ON \ -DFUN_WITH_CPP=ON \ + -DFUN_WITH_KCGI=ON \ -DFUN_WITH_OPENSSL=ON \ && cmake --build build --target fun elif [ "$target" = "all_debug" ]; then diff --git a/src/bytecode.h b/src/bytecode.h index 40f9b43..8bb4897 100644 --- a/src/bytecode.h +++ b/src/bytecode.h @@ -143,6 +143,12 @@ typedef enum { OP_PROC_SYSTEM, // pops command string; pushes exit code number OP_TIME_NOW_MS, // pushes current wall-clock time in milliseconds since Unix epoch OP_CLOCK_MONO_MS, // pushes monotonic clock in milliseconds (not wall time) + + // kcgi (optional) + OP_KCGI_PARSE, // () -> Map | Nil (parse request via kcgi) + OP_KCGI_REPLY_START, // (code:int, content_type:string) -> 1/0 + OP_KCGI_WRITE, // (chunk:string) -> 1/0 + OP_KCGI_END, // () -> 1/0 (free request) OP_DATE_FORMAT, // pops fmt string, ms epoch (int); pushes formatted date string using strftime OP_ENV_ALL, // pushes map of all environment variables OP_FUN_VERSION, // pushes version string diff --git a/src/extensions/kcgi.c b/src/extensions/kcgi.c new file mode 100644 index 0000000..2ce4607 --- /dev/null +++ b/src/extensions/kcgi.c @@ -0,0 +1,89 @@ +/* + * This file is part of the Fun programming language. + * https://fun-lang.xyz/ + * + * Copyright 2026 Johannes Findeisen + * Licensed under the terms of the Apache-2.0 license. + * https://opensource.org/license/apache-2-0 + */ + +/** + * @file kcgi.c + * @brief kcgi helpers for Fun VM KCGI-related opcodes (conditional build). + */ + +#ifdef FUN_WITH_KCGI + +#include +#include +#include + +/* Thread-local request handle for the current CGI invocation */ +#ifdef _WIN32 +static __declspec(thread) struct kreq *g_kcgi_req = NULL; +#else +static __thread struct kreq *g_kcgi_req = NULL; +#endif + +static Value kcgi_fields_to_map(const struct kreq *r) { + Value m = make_map_empty(); + if (!r) return m; + for (size_t i = 0; i < r->fieldsz; i++) { + const char *k = r->fields[i].key ? r->fields[i].key : ""; + const char *v = r->fields[i].val ? r->fields[i].val : ""; + map_set(&m, k, make_string(v)); + } + return m; +} + +static Value kreq_to_fun(const struct kreq *r) { + Value out = make_map_empty(); + if (!r) return out; + /* Basic request info */ + map_set(&out, "host", make_string(r->host ? r->host : "")); + map_set(&out, "port", make_int((int64_t)r->port)); + map_set(&out, "path", make_string(r->path ? r->path : "")); + map_set(&out, "suffix", make_string(r->suffix ? r->suffix : "")); + + Value fields = kcgi_fields_to_map(r); + map_set(&out, "fields", fields); + return out; +} + +/* Lifecycle helpers used by VM opcodes */ +static int kcgi_parse_request(struct kreq **out) { + static const struct kvalid keys[] = { { NULL, NULL } }; /* accept all */ + struct kreq *r = (struct kreq *)calloc(1, sizeof(*r)); + if (!r) return 0; + enum kcgi_err er = khttp_parse(r, keys, 0, NULL, 0, 0); + if (er != KCGI_OK) { free(r); return 0; } + *out = r; + return 1; +} + +static void kcgi_free_request(struct kreq *r) { + if (!r) return; + khttp_free(r); + free(r); +} + +static int kcgi_reply_start(int code, const char *ctype) { + if (!g_kcgi_req) return 0; + /* Emit Content-Type header; Status defaults to 200 if not set */ + if (ctype && *ctype) { + if (khttp_head(g_kcgi_req, "Content-Type", "%s", ctype) != KCGI_OK) + return 0; + } + if (khttp_body(g_kcgi_req) != KCGI_OK) + return 0; + return 1; +} + +static int kcgi_write_str(const char *s) { + if (!g_kcgi_req) return 0; + if (!s) s = ""; + size_t n = strlen(s); + return khttp_write(g_kcgi_req, s, n) == KCGI_OK; +} + +#endif /* FUN_WITH_KCGI */ diff --git a/src/parser.c b/src/parser.c index 1494e8a..3346115 100644 --- a/src/parser.c +++ b/src/parser.c @@ -1351,6 +1351,71 @@ static int emit_primary(Bytecode *bc, const char *src, size_t len, size_t *pos) free(name); return 1; } + /* KCGI intrinsics (optional feature; opcodes are safe no-ops when disabled) */ + if (strcmp(name, "kcgi_parse") == 0) { + (*pos)++; /* '(' */ + if (!consume_char(src, len, pos, ')')) { + parser_fail(*pos, "kcgi_parse expects ()"); + free(name); + return 0; + } + bytecode_add_instruction(bc, OP_KCGI_PARSE, 0); + free(name); + return 1; + } + if (strcmp(name, "kcgi_reply_start") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { + parser_fail(*pos, "kcgi_reply_start expects (code:int, content_type:string)"); + free(name); + return 0; + } + if (!consume_char(src, len, pos, ',')) { + parser_fail(*pos, "kcgi_reply_start expects 2 args"); + free(name); + return 0; + } + if (!emit_expression(bc, src, len, pos)) { + parser_fail(*pos, "kcgi_reply_start expects (code:int, content_type:string)"); + free(name); + return 0; + } + if (!consume_char(src, len, pos, ')')) { + parser_fail(*pos, "Expected ')' after kcgi_reply_start args"); + free(name); + return 0; + } + bytecode_add_instruction(bc, OP_KCGI_REPLY_START, 0); + free(name); + return 1; + } + if (strcmp(name, "kcgi_write") == 0) { + (*pos)++; /* '(' */ + if (!emit_expression(bc, src, len, pos)) { + parser_fail(*pos, "kcgi_write expects (chunk:string)"); + free(name); + return 0; + } + if (!consume_char(src, len, pos, ')')) { + parser_fail(*pos, "Expected ')' after kcgi_write arg"); + free(name); + return 0; + } + bytecode_add_instruction(bc, OP_KCGI_WRITE, 0); + free(name); + return 1; + } + if (strcmp(name, "kcgi_end") == 0) { + (*pos)++; /* '(' */ + if (!consume_char(src, len, pos, ')')) { + parser_fail(*pos, "kcgi_end expects ()"); + free(name); + return 0; + } + bytecode_add_instruction(bc, OP_KCGI_END, 0); + free(name); + return 1; + } if (strcmp(name, "rust_hello") == 0) { (*pos)++; /* '(' */ if (!consume_char(src, len, pos, ')')) { diff --git a/src/vm.c b/src/vm.c index 4827948..a74e039 100644 --- a/src/vm.c +++ b/src/vm.c @@ -72,6 +72,7 @@ #include "extensions/pcsc.c" #include "extensions/sqlite.c" #include "extensions/xml2.c" +#include "extensions/kcgi.c" /* forward declarations for include mapping used in error reporting */ extern char *preprocess_includes(const char *src); @@ -1089,6 +1090,14 @@ void vm_run(VM *vm, Bytecode *entry) { #include "vm/curl/post.c" #endif +/* KCGI ops */ +#ifdef FUN_WITH_KCGI +#include "vm/kcgi/parse.c" +#include "vm/kcgi/reply_start.c" +#include "vm/kcgi/write.c" +#include "vm/kcgi/end.c" +#endif + /* OpenSSL ops (md5/sha256/sha512/ripemd160) */ #ifdef FUN_WITH_OPENSSL #include "vm/openssl/md5.c" diff --git a/src/vm/kcgi/end.c b/src/vm/kcgi/end.c new file mode 100644 index 0000000..1bffcf5 --- /dev/null +++ b/src/vm/kcgi/end.c @@ -0,0 +1,10 @@ +/* KCGI_END */ +case OP_KCGI_END: { +#ifdef FUN_WITH_KCGI + if (g_kcgi_req) { kcgi_free_request(g_kcgi_req); g_kcgi_req = NULL; } + push_value(vm, make_int(1)); +#else + push_value(vm, make_int(0)); +#endif + break; +} diff --git a/src/vm/kcgi/parse.c b/src/vm/kcgi/parse.c new file mode 100644 index 0000000..594b423 --- /dev/null +++ b/src/vm/kcgi/parse.c @@ -0,0 +1,20 @@ +/* KCGI_PARSE */ +case OP_KCGI_PARSE: { +#ifdef FUN_WITH_KCGI + if (g_kcgi_req) { /* safety: free previous if any */ + kcgi_free_request(g_kcgi_req); + g_kcgi_req = NULL; + } + struct kreq *r = NULL; + if (!kcgi_parse_request(&r)) { + push_value(vm, make_nil()); + break; + } + g_kcgi_req = r; + Value v = kreq_to_fun(r); + push_value(vm, v); +#else + push_value(vm, make_nil()); +#endif + break; +} diff --git a/src/vm/kcgi/reply_start.c b/src/vm/kcgi/reply_start.c new file mode 100644 index 0000000..ce206cd --- /dev/null +++ b/src/vm/kcgi/reply_start.c @@ -0,0 +1,31 @@ +/* KCGI_REPLY_START */ +case OP_KCGI_REPLY_START: { +#ifdef FUN_WITH_KCGI + Value vct = pop_value(vm); + Value vcode = pop_value(vm); + int code = 200; + if (vcode.type == VAL_INT) { + code = (int)vcode.i; + } else if (vcode.type == VAL_FLOAT) { + code = (int)vcode.d; + } else if (vcode.type == VAL_STRING) { + if (vcode.s) code = atoi(vcode.s); + } + char *ct = value_to_string_alloc(&vct); + free_value(vcode); + free_value(vct); + if (!ct || ct[0] == '\0') { + /* default content type */ + free(ct); + ct = strdup("text/html; charset=utf-8"); + } + int ok = kcgi_reply_start(code, ct); + free(ct); + push_value(vm, make_int(ok ? 1 : 0)); +#else + Value a = pop_value(vm); free_value(a); + Value b = pop_value(vm); free_value(b); + push_value(vm, make_int(0)); +#endif + break; +} diff --git a/src/vm/kcgi/write.c b/src/vm/kcgi/write.c new file mode 100644 index 0000000..5473d33 --- /dev/null +++ b/src/vm/kcgi/write.c @@ -0,0 +1,15 @@ +/* KCGI_WRITE */ +case OP_KCGI_WRITE: { +#ifdef FUN_WITH_KCGI + Value vs = pop_value(vm); + char *s = value_to_string_alloc(&vs); + free_value(vs); + int ok = kcgi_write_str(s ? s : ""); + if (s) free(s); + push_value(vm, make_int(ok ? 1 : 0)); +#else + Value drop = pop_value(vm); free_value(drop); + push_value(vm, make_int(0)); +#endif + break; +} diff --git a/web/documentation/extensions/extensions.md b/web/documentation/extensions/extensions.md index 39be499..b38872c 100644 --- a/web/documentation/extensions/extensions.md +++ b/web/documentation/extensions/extensions.md @@ -47,6 +47,7 @@ This section documents Fun's optional, build-time selectable extensions. Each pa - [PCRE2 (Perl-compatible regex)](./pcre2/) - [PC/SC (Smart cards)](./pcsc/) - [OpenSSL](./openssl/) +- [kcgi](./kcgi/) ## Notes: diff --git a/web/documentation/extensions/ini/ini.md b/web/documentation/extensions/ini/ini.md index d8f89e0..94566e7 100644 --- a/web/documentation/extensions/ini/ini.md +++ b/web/documentation/extensions/ini/ini.md @@ -16,7 +16,6 @@ tags: - optional --- - - CMake option: FUN_WITH_INI=ON - Purpose: Read/write simple INI configuration files. - Homepage: [https://github.com/ndevilla/iniparser](https://github.com/ndevilla/iniparser){:class="ext"} diff --git a/web/documentation/extensions/json/json.md b/web/documentation/extensions/json/json.md index a2f0eb0..80e384a 100644 --- a/web/documentation/extensions/json/json.md +++ b/web/documentation/extensions/json/json.md @@ -15,7 +15,6 @@ tags: - optional --- - - CMake option: FUN_WITH_JSON=ON - Purpose: JSON parse/stringify and file helpers via json-c. - Homepage: [https://json-c.github.io/json-c/](https://json-c.github.io/json-c/){:class="ext"} diff --git a/web/documentation/extensions/kcgi/kcgi.md b/web/documentation/extensions/kcgi/kcgi.md new file mode 100644 index 0000000..45e9084 --- /dev/null +++ b/web/documentation/extensions/kcgi/kcgi.md @@ -0,0 +1,49 @@ +layout: page +published: true +noToc: false +noComments: false +noDate: false +title: Fun - KCGI extension (optional) +subtitle: Documentation for KCGI (kcgi) extension (optional) +description: Documentation for KCGI (kcgi) extension (optional) +permalink: /documentation/extensions/kcgi/ +lang: en +tags: +- extension +- kcgi +- optional +--- + +- CMake option: FUN_WITH_KCGI=ON +- Purpose: integrate the kcgi (CGI/FastCGI) C library; provide request parsing and response helpers for building CGI apps in Fun. +- Homepage: [https://kristaps.bsd.lv/kcgi/](https://kristaps.bsd.lv/kcgi/){:class="ext"} + +## Build notes: + +- Requires system kcgi development headers and libraries (pkg-config name: kcgi). +- If pkg-config is not available, build falls back to linking against libkcgi and zlib (as configured in cmake/Extensions/KCGI.cmake). + +## Provided helper/opcodes: + +- Function: kcgi_parse(data: none) -> Map | Nil. Parses the current CGI/FastCGI request via kcgi and returns a Map with keys like method, scheme, host, port, path, suffix, query, and fields (GET/POST map). Returns Nil on failure or when the extension is disabled. +- Function: kcgi_reply_start(code:int, content_type:string) -> 1/0. Starts the HTTP reply (sets Content-Type and opens the body). +- Function: kcgi_write(text:string) -> 1/0. Writes a chunk to the response body. +- Function: kcgi_end() -> 1/0. Finalizes the response and frees request resources. +- Opcodes: OP_KCGI_PARSE, OP_KCGI_REPLY_START, OP_KCGI_WRITE, OP_KCGI_END (internal mappings for the functions above). + +## Quickstart: + +- Configure: cmake -S . -B build -DFUN_WITH_KCGI=ON +- Build: cmake --build build --target fun +- Run example: + - FUN_LIB_DIR="$(pwd)/lib" ./build/fun examples/cgi/hello_kcgi.fun + +## Example output: + +- Content-Type: text/html; charset=utf-8 +-

Hello, Fun!

+ +## Notes: + +- Running outside a real CGI/FastCGI environment may emit RFC warnings (e.g., missing REMOTE_ADDR); these are benign for local testing. +- When FUN_WITH_KCGI is OFF, the helpers behave as no-ops (returning Nil/0) to keep scripts portable. diff --git a/web/documentation/extensions/openssl/openssl.md b/web/documentation/extensions/openssl/openssl.md index 9f19cdc..674e947 100644 --- a/web/documentation/extensions/openssl/openssl.md +++ b/web/documentation/extensions/openssl/openssl.md @@ -15,7 +15,6 @@ tags: - optional --- - - CMake option: FUN_WITH_OPENSSL=ON - Purpose: provide small crypto helpers backed by OpenSSL. Includes md5, sha256, sha512, ripemd160 helpers. - Homepage: [https://www.openssl.org/](https://www.openssl.org/){:class="ext"} diff --git a/web/documentation/extensions/pcre2/pcre2.md b/web/documentation/extensions/pcre2/pcre2.md index 5660d5e..3a6d449 100644 --- a/web/documentation/extensions/pcre2/pcre2.md +++ b/web/documentation/extensions/pcre2/pcre2.md @@ -18,7 +18,6 @@ tags: - regex --- - - CMake option: FUN_WITH_PCRE2=ON - Purpose: Advanced regular expressions via PCRE2. - Homepage: [https://www.pcre.org/](https://www.pcre.org/){:class="ext"} diff --git a/web/documentation/extensions/pcsc/pcsc.md b/web/documentation/extensions/pcsc/pcsc.md index 2929157..e3a7fca 100644 --- a/web/documentation/extensions/pcsc/pcsc.md +++ b/web/documentation/extensions/pcsc/pcsc.md @@ -16,7 +16,6 @@ tags: - smart --- - - CMake option: FUN_WITH_PCSC=ON - Purpose: Access smart card readers/cards via PC/SC (pcsclite). - Homepage: [https://pcsclite.apdu.fr/](https://pcsclite.apdu.fr/){:class="ext"} diff --git a/web/documentation/extensions/sqlite/sqlite.md b/web/documentation/extensions/sqlite/sqlite.md index b17d36a..8ec0e2b 100644 --- a/web/documentation/extensions/sqlite/sqlite.md +++ b/web/documentation/extensions/sqlite/sqlite.md @@ -15,7 +15,6 @@ tags: - sqlite --- - - CMake option: FUN_WITH_SQLITE=ON - Purpose: Access SQLite databases via the native C API. - Homepage: [https://www.sqlite.org/](https://www.sqlite.org/){:class="ext"} diff --git a/web/documentation/extensions/xml2/xml2.md b/web/documentation/extensions/xml2/xml2.md index 75ae946..df21e3c 100644 --- a/web/documentation/extensions/xml2/xml2.md +++ b/web/documentation/extensions/xml2/xml2.md @@ -16,7 +16,6 @@ tags: - xml --- - - CMake option: FUN_WITH_XML2=ON - Purpose: Minimal XML parsing helpers using libxml2. - Homepage: [http://xmlsoft.org/](http://xmlsoft.org/){:class="ext"}