blob: 632eb358737248f00b5348997bb3b0a06b9f7915 [file] [log] [blame]
/* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include "charmony.h"
#include <string.h>
#include <ctype.h>
#include <cmark.h>
#define CFC_NEED_BASE_STRUCT_DEF
#include "CFCBase.h"
#include "CFCPerlPod.h"
#include "CFCUtil.h"
#include "CFCClass.h"
#include "CFCMethod.h"
#include "CFCParcel.h"
#include "CFCParamList.h"
#include "CFCPerlMethod.h"
#include "CFCFunction.h"
#include "CFCDocuComment.h"
#include "CFCUri.h"
#include "CFCDocument.h"
#include "CFCType.h"
#include "CFCVariable.h"
#include "CFCCallable.h"
#ifndef true
#define true 1
#define false 0
#endif
typedef struct NamePod {
char *alias;
char *func;
char *sample;
char *pod;
} NamePod;
struct CFCPerlPod {
CFCBase base;
char *synopsis;
char *description;
NamePod *methods;
size_t num_methods;
NamePod *constructors;
size_t num_constructors;
};
static const CFCMeta CFCPERLPOD_META = {
"Clownfish::CFC::Binding::Perl::Pod",
sizeof(CFCPerlPod),
(CFCBase_destroy_t)CFCPerlPod_destroy
};
static char*
S_gen_code_sample(CFCCallable *func, const char *alias, CFCClass *klass,
int is_constructor);
static char*
S_gen_positional_sample(const char *invocant, const char *alias,
CFCParamList *param_list, size_t start);
static char*
S_gen_labeled_sample(const char *invocant, const char *alias,
CFCParamList *param_list, size_t start);
static char*
S_perl_var_name(CFCVariable *var);
static char*
S_camel_to_lower(const char *camel);
static char*
S_nodes_to_pod(cmark_node *node, CFCClass *klass, int header_level);
static char*
S_node_to_pod(cmark_node *node, CFCClass *klass, int header_level);
static char*
S_pod_escape(const char *content);
static char*
S_convert_link(cmark_node *link, CFCClass *klass, int header_level);
static char*
S_pod_link(const char *text, const char *name);
CFCPerlPod*
CFCPerlPod_new(void) {
CFCPerlPod *self
= (CFCPerlPod*)CFCBase_allocate(&CFCPERLPOD_META);
return CFCPerlPod_init(self);
}
CFCPerlPod*
CFCPerlPod_init(CFCPerlPod *self) {
self->synopsis = CFCUtil_strdup("");
self->description = CFCUtil_strdup("");
self->methods = NULL;
self->constructors = NULL;
self->num_methods = 0;
self->num_constructors = 0;
return self;
}
void
CFCPerlPod_destroy(CFCPerlPod *self) {
FREEMEM(self->synopsis);
FREEMEM(self->description);
for (size_t i = 0; i < self->num_methods; i++) {
FREEMEM(self->methods[i].alias);
FREEMEM(self->methods[i].pod);
FREEMEM(self->methods[i].func);
FREEMEM(self->methods[i].sample);
}
FREEMEM(self->methods);
for (size_t i = 0; i < self->num_constructors; i++) {
FREEMEM(self->constructors[i].alias);
FREEMEM(self->constructors[i].pod);
FREEMEM(self->constructors[i].func);
FREEMEM(self->constructors[i].sample);
}
FREEMEM(self->constructors);
CFCBase_destroy((CFCBase*)self);
}
void
CFCPerlPod_add_method(CFCPerlPod *self, const char *alias, const char *method,
const char *sample, const char *pod) {
CFCUTIL_NULL_CHECK(alias);
self->num_methods++;
size_t size = self->num_methods * sizeof(NamePod);
self->methods = (NamePod*)REALLOCATE(self->methods, size);
NamePod *slot = &self->methods[self->num_methods - 1];
slot->alias = CFCUtil_strdup(alias);
slot->func = method ? CFCUtil_strdup(method) : NULL;
slot->sample = sample ? CFCUtil_strdup(sample) : NULL;
slot->pod = pod ? CFCUtil_strdup(pod) : NULL;
}
void
CFCPerlPod_add_constructor(CFCPerlPod *self, const char *alias,
const char *pod_func, const char *sample,
const char *pod) {
self->num_constructors++;
size_t size = self->num_constructors * sizeof(NamePod);
self->constructors = (NamePod*)REALLOCATE(self->constructors, size);
NamePod *slot = &self->constructors[self->num_constructors - 1];
slot->alias = CFCUtil_strdup(alias ? alias : "new");
slot->func = pod_func ? CFCUtil_strdup(pod_func) : NULL;
slot->sample = sample ? CFCUtil_strdup(sample) : NULL;
slot->pod = pod ? CFCUtil_strdup(pod) : NULL;
}
void
CFCPerlPod_set_synopsis(CFCPerlPod *self, const char *synopsis) {
FREEMEM(self->synopsis);
self->synopsis = CFCUtil_strdup(synopsis);
}
const char*
CFCPerlPod_get_synopsis(CFCPerlPod *self) {
return self->synopsis;
}
void
CFCPerlPod_set_description(CFCPerlPod *self, const char *description) {
FREEMEM(self->description);
self->description = CFCUtil_strdup(description);
}
const char*
CFCPerlPod_get_description(CFCPerlPod *self) {
return self->description;
}
char*
CFCPerlPod_methods_pod(CFCPerlPod *self, CFCClass *klass) {
const char *class_name = CFCClass_get_name(klass);
char *abstract_pod = CFCUtil_strdup("");
char *methods_pod = CFCUtil_strdup("");
// Start with methods that don't map to a Clownfish method.
for (size_t i = 0; i < self->num_methods; i++) {
NamePod meth_spec = self->methods[i];
CFCMethod *method = CFCClass_method(klass, meth_spec.func);
if (method) { continue; }
if (!meth_spec.pod) {
CFCUtil_die("No POD specified for method '%s' in class '%s'",
meth_spec.alias, CFCClass_get_name(klass));
}
methods_pod = CFCUtil_cat(methods_pod, meth_spec.pod, "\n", NULL);
}
CFCMethod **fresh_methods = CFCClass_fresh_methods(klass);
for (int meth_num = 0; fresh_methods[meth_num] != NULL; meth_num++) {
CFCMethod *method = fresh_methods[meth_num];
const char *name = CFCMethod_get_name(method);
char *meth_pod = NULL;
// Try to find custom POD for method.
NamePod *meth_spec = NULL;
for (size_t j = 0; j < self->num_methods; j++) {
NamePod *candidate = &self->methods[j];
const char *other_name = candidate->func;
if (other_name && strcmp(other_name, name) == 0) {
meth_spec = candidate;
break;
}
}
if (meth_spec) {
// Found custom POD.
if (meth_spec->pod) {
meth_pod = CFCUtil_sprintf("%s\n", meth_spec->pod);
}
else {
meth_pod
= CFCPerlPod_gen_subroutine_pod((CFCCallable*)method,
meth_spec->alias, klass,
meth_spec->sample,
class_name, false);
}
}
else {
// No custom POD found. Add POD for public methods with Perl
// bindings.
if (!CFCMethod_public(method)
|| CFCMethod_excluded_from_host(method)
|| !CFCMethod_can_be_bound(method)
) {
continue;
}
// Only add POD for novel methods and the first implementation
// of abstract methods.
if (!CFCMethod_novel(method)) {
if (CFCMethod_abstract(method)) { continue; }
CFCClass *parent = CFCClass_get_parent(klass);
CFCMethod *parent_method = CFCClass_method(parent, name);
if (!CFCMethod_abstract(parent_method)) { continue; }
}
char *perl_name = CFCPerlMethod_perl_name(method);
meth_pod
= CFCPerlPod_gen_subroutine_pod((CFCCallable*)method,
perl_name, klass, NULL,
class_name, false);
FREEMEM(perl_name);
}
if (CFCMethod_abstract(method)) {
abstract_pod = CFCUtil_cat(abstract_pod, meth_pod, NULL);
}
else {
methods_pod = CFCUtil_cat(methods_pod, meth_pod, NULL);
}
FREEMEM(meth_pod);
}
char *pod = CFCUtil_strdup("");
if (strlen(abstract_pod)) {
pod = CFCUtil_cat(pod, "=head1 ABSTRACT METHODS\n\n", abstract_pod, NULL);
}
FREEMEM(abstract_pod);
if (strlen(methods_pod)) {
pod = CFCUtil_cat(pod, "=head1 METHODS\n\n", methods_pod, NULL);
}
FREEMEM(methods_pod);
return pod;
}
char*
CFCPerlPod_constructors_pod(CFCPerlPod *self, CFCClass *klass) {
if (!self->num_constructors) {
return CFCUtil_strdup("");
}
const char *class_name = CFCClass_get_name(klass);
char *pod = CFCUtil_strdup("=head1 CONSTRUCTORS\n\n");
for (size_t i = 0; i < self->num_constructors; i++) {
NamePod slot = self->constructors[i];
if (slot.pod) {
pod = CFCUtil_cat(pod, slot.pod, "\n", NULL);
}
else {
const char *func_name = slot.func ? slot.func : slot.alias;
CFCFunction *pod_func = CFCClass_function(klass, func_name);
if (!pod_func) {
CFCUtil_die("Can't find constructor '%s' in class '%s'",
func_name, CFCClass_get_name(klass));
}
char *sub_pod
= CFCPerlPod_gen_subroutine_pod((CFCCallable*)pod_func,
slot.alias, klass, slot.sample,
class_name, true);
pod = CFCUtil_cat(pod, sub_pod, NULL);
FREEMEM(sub_pod);
}
}
return pod;
}
char*
CFCPerlPod_gen_subroutine_pod(CFCCallable *func,
const char *alias, CFCClass *klass,
const char *code_sample,
const char *class_name, int is_constructor) {
const char *func_name = CFCCallable_get_name(func);
// Only allow "public" subs to be exposed as part of the public API.
if (!CFCCallable_public(func)) {
CFCUtil_die("%s#%s is not public", class_name, func_name);
}
char *pod = CFCUtil_sprintf("=head2 %s\n\n", alias);
// Add code sample.
if (!code_sample) {
char *auto_sample
= S_gen_code_sample(func, alias, klass, is_constructor);
pod = CFCUtil_cat(pod, auto_sample, "\n", NULL);
FREEMEM(auto_sample);
}
else {
pod = CFCUtil_cat(pod, code_sample, "\n", NULL);
}
// Get documentation, which may be inherited.
CFCDocuComment *docucomment = CFCCallable_get_docucomment(func);
if (!docucomment) {
CFCClass *parent = klass;
while (NULL != (parent = CFCClass_get_parent(parent))) {
CFCCallable *parent_func
= (CFCCallable*)CFCClass_method(parent, func_name);
if (!parent_func) { break; }
docucomment = CFCCallable_get_docucomment(parent_func);
if (docucomment) { break; }
}
}
if (!docucomment) {
return pod;
}
// Incorporate "description" text from DocuComment.
const char *long_doc = CFCDocuComment_get_description(docucomment);
if (long_doc && strlen(long_doc)) {
char *perlified = CFCPerlPod_md_to_pod(long_doc, klass, 3);
pod = CFCUtil_cat(pod, perlified, NULL);
FREEMEM(perlified);
}
// Add params in a list.
const char**param_names = CFCDocuComment_get_param_names(docucomment);
const char**param_docs = CFCDocuComment_get_param_docs(docucomment);
if (param_names[0]) {
pod = CFCUtil_cat(pod, "=over\n\n", NULL);
for (size_t i = 0; param_names[i] != NULL; i++) {
char *perlified = CFCPerlPod_md_to_pod(param_docs[i], klass, 3);
pod = CFCUtil_cat(pod, "=item *\n\nB<", param_names[i], "> - ",
perlified, NULL);
FREEMEM(perlified);
}
pod = CFCUtil_cat(pod, "=back\n\n", NULL);
}
// Add return value description, if any.
const char *retval_doc = CFCDocuComment_get_retval(docucomment);
if (retval_doc && strlen(retval_doc)) {
char *perlified = CFCPerlPod_md_to_pod(retval_doc, klass, 3);
pod = CFCUtil_cat(pod, "Returns: ", perlified, NULL);
FREEMEM(perlified);
}
return pod;
}
static char*
S_gen_code_sample(CFCCallable *func, const char *alias, CFCClass *klass,
int is_constructor) {
char *invocant = NULL;
if (is_constructor) {
invocant = CFCUtil_strdup(CFCClass_get_name(klass));
}
else {
char *lower = S_camel_to_lower(CFCClass_get_struct_sym(klass));
invocant = CFCUtil_sprintf("$%s", lower);
FREEMEM(lower);
}
CFCParamList *param_list = CFCCallable_get_param_list(func);
size_t num_vars = CFCParamList_num_vars(param_list);
size_t start = is_constructor ? 0 : 1;
char *sample = NULL;
if (start == num_vars) {
sample = CFCUtil_sprintf(" %s->%s();\n", invocant, alias);
}
else if (is_constructor || num_vars - start >= 2) {
sample = S_gen_labeled_sample(invocant, alias, param_list, start);
}
else {
sample = S_gen_positional_sample(invocant, alias, param_list, start);
}
return sample;
}
static char*
S_gen_positional_sample(const char *invocant, const char *alias,
CFCParamList *param_list, size_t start) {
size_t num_vars = CFCParamList_num_vars(param_list);
CFCVariable **vars = CFCParamList_get_variables(param_list);
const char **inits = CFCParamList_get_initial_values(param_list);
if (num_vars - start != 1) {
CFCUtil_die("Code samples with multiple positional parameters"
" are not supported yet.");
}
const char *name = CFCVariable_get_name(vars[start]);
char *sample
= CFCUtil_sprintf(" %s->%s($%s);\n", invocant, alias, name);
const char *init = inits[start];
if (init) {
if (strcmp(init, "NULL") == 0) { init = "undef"; }
char *def_sample = CFCUtil_sprintf(" %s->%s(); # default: %s\n",
invocant, alias, init);
sample = CFCUtil_cat(sample, def_sample, NULL);
FREEMEM(def_sample);
}
return sample;
}
static char*
S_gen_labeled_sample(const char *invocant, const char *alias,
CFCParamList *param_list, size_t start) {
size_t num_vars = CFCParamList_num_vars(param_list);
CFCVariable **vars = CFCParamList_get_variables(param_list);
const char **inits = CFCParamList_get_initial_values(param_list);
size_t max_label_len = 0;
size_t max_var_len = 0;
// Find maximum length of label and Perl variable.
for (size_t i = start; i < num_vars; i++) {
CFCVariable *var = vars[i];
const char *label = CFCVariable_get_name(var);
size_t label_len = strlen(label);
if (label_len > max_label_len) { max_label_len = label_len; }
char *perl_var = S_perl_var_name(var);
size_t perl_var_len = strlen(perl_var);
if (perl_var_len > max_var_len) { max_var_len = perl_var_len; }
FREEMEM(perl_var);
}
char *params = CFCUtil_strdup("");
for (size_t i = start; i < num_vars; i++) {
CFCVariable *var = vars[i];
const char *label = CFCVariable_get_name(var);
char *perl_var = S_perl_var_name(var);
perl_var = CFCUtil_cat(perl_var, ",", NULL);
char *comment = NULL;
const char *init = inits[i];
if (init) {
if (strcmp(init, "NULL") == 0) { init = "undef"; }
comment = CFCUtil_sprintf("default: %s", init);
}
else {
comment = CFCUtil_strdup("required");
}
char *line = CFCUtil_sprintf(" %-*s => $%-*s # %s\n",
max_label_len, label,
max_var_len + 1, perl_var,
comment);
params = CFCUtil_cat(params, line, NULL);
FREEMEM(line);
FREEMEM(comment);
FREEMEM(perl_var);
}
const char pattern[] =
" %s->%s(\n"
"%s"
" );\n";
char *sample = CFCUtil_sprintf(pattern, invocant, alias, params);
FREEMEM(params);
return sample;
}
static char*
S_perl_var_name(CFCVariable *var) {
CFCType *type = CFCVariable_get_type(var);
const char *specifier = CFCType_get_specifier(type);
char *perl_name = NULL;
if (CFCType_is_object(type)) {
// Skip parcel prefix.
if (islower(*specifier)) {
for (specifier++; *specifier; specifier++) {
if (*specifier == '_') {
specifier++;
break;
}
}
}
perl_name = S_camel_to_lower(specifier);
}
else if (CFCType_is_integer(type)) {
if (strcmp(specifier, "bool") == 0) {
perl_name = CFCUtil_strdup("bool");
}
else {
perl_name = CFCUtil_strdup("int");
}
}
else if (CFCType_is_floating(type)) {
perl_name = CFCUtil_strdup("float");
}
else {
CFCUtil_die("Don't know how to create code sample for type '%s'",
specifier);
}
return perl_name;
}
static char*
S_camel_to_lower(const char *camel) {
if (camel[0] == '\0') { return CFCUtil_strdup(""); }
size_t alloc = 1;
for (size_t i = 1; camel[i]; i++) {
if (isupper(camel[i]) && islower(camel[i+1])) {
alloc += 1;
}
alloc += 1;
}
char *lower = (char*)MALLOCATE(alloc + 1);
lower[0] = tolower(camel[0]);
size_t j = 1;
for (size_t i = 1; camel[i]; i++) {
// Only insert underscore if next char is lowercase.
if (isupper(camel[i]) && islower(camel[i+1])) {
lower[j++] = '_';
}
lower[j++] = tolower(camel[i]);
}
lower[j] = '\0';
return lower;
}
char*
CFCPerlPod_md_doc_to_pod(const char *module, const char *md) {
int options = CMARK_OPT_SMART
| CMARK_OPT_VALIDATE_UTF8;
cmark_node *doc = cmark_parse_document(md, strlen(md), options);
cmark_node *maybe_header = cmark_node_first_child(doc);
char *name;
char *desc;
if (maybe_header
&& cmark_node_get_type(maybe_header) == CMARK_NODE_HEADER
) {
cmark_node *header_child = cmark_node_first_child(maybe_header);
char *short_desc = S_nodes_to_pod(header_child, NULL, 1);
name = CFCUtil_sprintf("%s - %s", module, short_desc);
cmark_node *remaining = cmark_node_next(maybe_header);
desc = S_nodes_to_pod(remaining, NULL, 1);
}
else {
// No header found.
name = CFCUtil_strdup(module);
desc = S_node_to_pod(doc, NULL, 1);
}
const char *pattern =
"=head1 NAME\n"
"\n"
"%s\n"
"\n"
"=head1 DESCRIPTION\n"
"\n"
"%s";
char *retval = CFCUtil_sprintf(pattern, name, desc);
FREEMEM(name);
FREEMEM(desc);
return retval;
}
char*
CFCPerlPod_md_to_pod(const char *md, CFCClass *klass, int header_level) {
int options = CMARK_OPT_SMART
| CMARK_OPT_VALIDATE_UTF8;
cmark_node *doc = cmark_parse_document(md, strlen(md), options);
char *pod = S_node_to_pod(doc, klass, header_level);
cmark_node_free(doc);
return pod;
}
// Convert a node and its siblings.
static char*
S_nodes_to_pod(cmark_node *node, CFCClass *klass, int header_level) {
char *result = CFCUtil_strdup("");
while (node != NULL) {
char *pod = S_node_to_pod(node, klass, header_level);
result = CFCUtil_cat(result, pod, NULL);
FREEMEM(pod);
node = cmark_node_next(node);
}
return result;
}
// Convert a single node.
static char*
S_node_to_pod(cmark_node *node, CFCClass *klass, int header_level) {
char *result = CFCUtil_strdup("");
if (node == NULL) {
return result;
}
int found_matching_code_block = false;
cmark_iter *iter = cmark_iter_new(node);
cmark_event_type ev_type;
while (CMARK_EVENT_DONE != (ev_type = cmark_iter_next(iter))) {
cmark_node *node = cmark_iter_get_node(iter);
cmark_node_type type = cmark_node_get_type(node);
switch (type) {
case CMARK_NODE_DOCUMENT:
break;
case CMARK_NODE_PARAGRAPH:
if (ev_type == CMARK_EVENT_EXIT) {
result = CFCUtil_cat(result, "\n\n", NULL);
}
break;
case CMARK_NODE_BLOCK_QUOTE:
case CMARK_NODE_LIST:
if (ev_type == CMARK_EVENT_ENTER) {
result = CFCUtil_cat(result, "=over\n\n", NULL);
}
else {
result = CFCUtil_cat(result, "=back\n\n", NULL);
}
break;
case CMARK_NODE_ITEM:
// TODO: Ordered lists.
if (ev_type == CMARK_EVENT_ENTER) {
result = CFCUtil_cat(result, "=item *\n\n", NULL);
}
break;
case CMARK_NODE_HEADER:
if (ev_type == CMARK_EVENT_ENTER) {
int extra_level = cmark_node_get_header_level(node) - 1;
char *header = CFCUtil_sprintf("=head%d ",
header_level + extra_level);
result = CFCUtil_cat(result, header, NULL);
FREEMEM(header);
}
else {
result = CFCUtil_cat(result, "\n\n", NULL);
}
break;
case CMARK_NODE_CODE_BLOCK: {
int is_host = CFCMarkdown_code_block_is_host(node, "perl");
if (is_host) {
found_matching_code_block = true;
const char *content = cmark_node_get_literal(node);
char *copy = CFCUtil_strdup(content);
// Chomp trailing newline.
size_t len = strlen(copy);
if (len > 0 && copy[len-1] == '\n') {
copy[len-1] = '\0';
}
char *indented
= CFCUtil_global_replace(copy, "\n", "\n ");
result
= CFCUtil_cat(result, " ", indented, "\n\n", NULL);
FREEMEM(indented);
FREEMEM(copy);
}
if (CFCMarkdown_code_block_is_last(node)) {
if (!found_matching_code_block) {
result = CFCUtil_cat(result,
" Code example for Perl is missing\n\n");
}
else {
// Reset.
found_matching_code_block = false;
}
}
break;
}
case CMARK_NODE_HTML: {
const char *html = cmark_node_get_literal(node);
result = CFCUtil_cat(result, "=begin html\n\n", html,
"\n=end\n\n", NULL);
break;
}
case CMARK_NODE_HRULE:
break;
case CMARK_NODE_TEXT: {
const char *content = cmark_node_get_literal(node);
char *escaped = S_pod_escape(content);
result = CFCUtil_cat(result, escaped, NULL);
FREEMEM(escaped);
break;
}
case CMARK_NODE_LINEBREAK:
// POD doesn't support line breaks. Start a new paragraph.
result = CFCUtil_cat(result, "\n\n", NULL);
break;
case CMARK_NODE_SOFTBREAK:
result = CFCUtil_cat(result, "\n", NULL);
break;
case CMARK_NODE_CODE: {
const char *content = cmark_node_get_literal(node);
char *escaped = S_pod_escape(content);
result = CFCUtil_cat(result, "C<", escaped, ">", NULL);
FREEMEM(escaped);
break;
}
case CMARK_NODE_INLINE_HTML: {
const char *html = cmark_node_get_literal(node);
CFCUtil_warn("Inline HTML not supported in POD: %s", html);
break;
}
case CMARK_NODE_LINK:
if (ev_type == CMARK_EVENT_ENTER) {
char *pod = S_convert_link(node, klass, header_level);
result = CFCUtil_cat(result, pod, NULL);
FREEMEM(pod);
cmark_iter_reset(iter, node, CMARK_EVENT_EXIT);
}
break;
case CMARK_NODE_IMAGE:
CFCUtil_warn("Images not supported in POD");
break;
case CMARK_NODE_STRONG:
if (ev_type == CMARK_EVENT_ENTER) {
result = CFCUtil_cat(result, "B<", NULL);
}
else {
result = CFCUtil_cat(result, ">", NULL);
}
break;
case CMARK_NODE_EMPH:
if (ev_type == CMARK_EVENT_ENTER) {
result = CFCUtil_cat(result, "I<", NULL);
}
else {
result = CFCUtil_cat(result, ">", NULL);
}
break;
default:
CFCUtil_die("Invalid cmark node type: %d", type);
break;
}
}
cmark_iter_free(iter);
return result;
}
static char*
S_pod_escape(const char *content) {
size_t len = strlen(content);
size_t result_len = 0;
size_t result_cap = len + 256;
char *result = (char*)MALLOCATE(result_cap + 1);
for (size_t i = 0; i < len; i++) {
const char *subst = content + i;
size_t subst_size = 1;
switch (content[i]) {
case '<':
// Escape "less than".
subst = "E<lt>";
subst_size = 5;
break;
case '>':
// Escape "greater than".
subst = "E<gt>";
subst_size = 5;
break;
case '|':
// Escape vertical bar.
subst = "E<verbar>";
subst_size = 9;
break;
case '=':
// Escape equal sign at start of line.
if (i == 0 || content[i-1] == '\n') {
subst = "E<61>";
subst_size = 5;
}
break;
default:
break;
}
if (result_len + subst_size > result_cap) {
result_cap += 256;
result = (char*)REALLOCATE(result, result_cap + 1);
}
memcpy(result + result_len, subst, subst_size);
result_len += subst_size;
}
result[result_len] = '\0';
return result;
}
static char*
S_convert_link(cmark_node *link, CFCClass *doc_class, int header_level) {
cmark_node *child = cmark_node_first_child(link);
const char *uri = cmark_node_get_url(link);
char *text = S_nodes_to_pod(child, doc_class, header_level);
char *retval;
if (!CFCUri_is_clownfish_uri(uri)) {
retval = S_pod_link(text, uri);
FREEMEM(text);
return retval;
}
char *new_uri = NULL;
char *new_text = NULL;
CFCUri *uri_obj = CFCUri_new(uri, doc_class);
CFCUriType type = CFCUri_get_type(uri_obj);
switch (type) {
case CFC_URI_ERROR: {
const char *error = CFCUri_get_error(uri_obj);
new_text = CFCUtil_sprintf("[%s]", error);
break;
}
case CFC_URI_NULL:
// Change all instances of NULL to 'undef'
new_text = CFCUtil_strdup("undef");
break;
case CFC_URI_CLASS: {
CFCClass *klass = CFCUri_get_class(uri_obj);
if (klass != doc_class) {
const char *class_name = CFCClass_get_name(klass);
new_uri = CFCUtil_strdup(class_name);
}
if (text[0] == '\0') {
const char *src = CFCClass_included(klass)
? CFCClass_get_name(klass)
: CFCClass_get_struct_sym(klass);
new_text = CFCUtil_strdup(src);
}
break;
}
case CFC_URI_FUNCTION:
case CFC_URI_METHOD: {
CFCClass *klass = CFCUri_get_class(uri_obj);
const char *name = CFCUri_get_callable_name(uri_obj);
// Convert "Err_get_error" to "Clownfish->error".
if (strcmp(CFCClass_full_struct_sym(klass), "cfish_Err") == 0
&& strcmp(name, "get_error") == 0
) {
new_text = CFCUtil_strdup("Clownfish->error");
break;
}
char *perl_name = CFCUtil_strdup(name);
for (size_t i = 0; perl_name[i] != '\0'; ++i) {
perl_name[i] = tolower(perl_name[i]);
}
// The Perl POD only contains sections for novel methods. Link
// to the class where the method is declared first.
if (type == CFC_URI_METHOD) {
CFCClass *parent = CFCClass_get_parent(klass);
while (parent && CFCClass_method(parent, name)) {
klass = parent;
parent = CFCClass_get_parent(klass);
}
}
if (klass == doc_class) {
new_uri = CFCUtil_sprintf("/%s", perl_name);
}
else {
const char *class_name = CFCClass_get_name(klass);
new_uri = CFCUtil_sprintf("%s/%s", class_name, perl_name);
}
if (text[0] == '\0') {
new_text = CFCUtil_sprintf("%s()", perl_name);
}
FREEMEM(perl_name);
break;
}
case CFC_URI_DOCUMENT: {
CFCDocument *doc = CFCUri_get_document(uri_obj);
const char *path_part = CFCDocument_get_path_part(doc);
new_uri = CFCUtil_global_replace(path_part, CHY_DIR_SEP, "::");
if (text[0] == '\0') {
const char *name = CFCDocument_get_name(doc);
new_text = CFCUtil_strdup(name);
}
break;
}
}
if (new_text) {
FREEMEM(text);
text = new_text;
}
if (new_uri) {
retval = S_pod_link(text, new_uri);
FREEMEM(new_uri);
FREEMEM(text);
}
else {
retval = text;
}
CFCBase_decref((CFCBase*)uri_obj);
return retval;
}
static char*
S_pod_link(const char *text, const char *name) {
if (!text || text[0] == '\0' || strcmp(text, name) == 0) {
return CFCUtil_sprintf("L<%s>", name);
}
else {
return CFCUtil_sprintf("L<%s|%s>", text, name);
}
}