Q

破公司的设计文档终于决定要从word改为markdown了。其中画UML的工具是plantuml,写个工具让我能够在linux侧编辑,windows侧看到效果。同时博客这边也支持一下这个功能,后面应该能掌握这个工具。

~

官方提供的转换工具是一个jar包,随便选择一个开源协议下载。

这个网站会弹窗通知权限,通过浏览器给你推送广告了。如果点了同意,记得在设置里取消。

演示一下如何使用,比如说以下的例子:

@startuml
start
if (legel) then (T)
    :process;
else
    :report error;
endif
end
@enduml

保存进一个文件里,然后使用命令行生成图片:

java -jar puml.jar tmp.txt --svg

# 使用流输入
cat tmp.txt |java -jar puml.jar --svg --pipe > tmp.svg

java -jar puml.jar --svg --pipe > tmp.svg <<EOF
@startuml
start
if (legel) then (T)
    :process;
else
    :report error;
endif
end
@enduml
EOF

之会在当前目录下生成一个tmp.svg(这里如果我把活干完了,这里应该能正常显示)。

legelTprocessreport error

因为不想要把图片额外存储,所以会使用svg。初步设想总体流程如下。

转换plantuml开一个子进程用java转换为svg用svg替换掉代码块

但是这个步骤可以放在不同的阶段去做,比如说md2html是处理markdown的主要流程,按照不同的处理时机节点,可以分成三种。

前处理转换plantumlmd2html处理处理时md2html处理开始魔改代码,处理代码块时转换plantumlmd2html处理结束后处理md2html处理转换code块language-plantuml的块

第二种的修改难度是最大的,需要理解md4c的原理,修改解析回调函数代码。md4c是一套很高效、优雅的解析框架,读它代码的成本不低,后面可以研究一下,这里有篇记录可以作为原理参考,todolisting。

第三种,后处理依赖于解析框架,我已经见过有些解析工具会把codeblock包装成li,这下依赖关系就开始变得恶心了。

所以我决定采用第一种修改方式。

A

由于sapin还没对外开源,这里就不写patch了。我额外生成了一个可以单独执行的二进制,可以套在md2html之前做,下面是库的代码。

// puml.h
#ifndef PUML_H
#define PUML_H
#ifdef __cplusplus
extern "C" {
#endif /* __cplusplus */

typedef struct {
    const char *input;
    size_t len;
} puml_input;

typedef void (*puml_out_fn)(void*, const char *buf, size_t buflen);

int puml2svg(
        const puml_input *,
        void *,
        puml_out_fn
);




#ifdef __cplusplus
}
#endif /* __cplusplus */
#endif /* PUML_H */
// puml.c
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdio.h>
#include <string.h>

#include "puml.h"

#define MIN(a, b) ((a) > (b)) ? (b) : (a)
#define PUML_START  "```plantuml\n"
#define PUML_END    "```"

enum puml_status {
    NORMAL,
    IN_PUML,
};

static int run_jar (
        const char *jar,
        const char *in,
        size_t ilen,
        void *out,
        puml_out_fn out_fn)
{
    int p2c[2];
    int c2p[2];
    if (pipe(p2c) < 0) return 1;
    if (pipe(c2p) < 0) return 1;
    pid_t pid = fork();
    if (pid == 0) {
        dup2(p2c[0], STDIN_FILENO);
        dup2(c2p[1], STDOUT_FILENO);

        close(p2c[0]);
        close(p2c[1]);
        close(c2p[0]);
        close(c2p[1]);

        execlp("java", "java", "-jar", jar, "--pipe", "--svg", NULL);
        _exit(1);
    } else if (pid < 0) {
        return 1;
    }
    close(p2c[0]);
    close(c2p[1]);

    /* input */
    write(p2c[1], in, ilen);
    close(p2c[1]);

    /* output */
    char buf[4096];
    ssize_t n;
    while ((n = read(c2p[0], buf, sizeof(buf))) > 0) {
        out_fn(out, buf, n);
    }
    char br[] = "\n<br>\n";
    out_fn(out, br, strlen(br));
    if (n < 0) {
        close(c2p[0]);
        printf("read jar stdout get n = %d\n", n);
        waitpid(pid, NULL, 0);
        return 1;
    }
    close(c2p[0]);
    waitpid(pid, NULL, 0);
    return 0;
}

static int judge_same (
        const char *l,
        size_t llen,
        const char *r,
        size_t rlen)
{
    if (llen != rlen) return 0;
    while (llen > 0) {
        llen--;
        if (l[llen] != r[llen]) return 0;
    }
    return 1;
}

int puml2svg(
        const puml_input *input,
        void *output,
        puml_out_fn ouput_fn)
{
    const char *ptr = input->input;
    const char *optr = ptr;
    const char *in;
    const char *jar_path = getenv("PUML_JAR");
    size_t ilen = 0;
    size_t isize = 4096;
    enum puml_status status = NORMAL;
    int ret;
    if (!jar_path) {
        fprintf(stderr, "need env 'PUML_JAR'\n");
        return 1;
    }
    while (ptr < (input->input + input->len)) {
        if (*ptr == '\n' || (ptr + 1) == (input->input + input->len)) {
            size_t len = (ptr - optr + 1);
            if (status == NORMAL) {
                if (judge_same(optr, len, PUML_START, strlen(PUML_START))) {
                    in = ptr + 1;
                    status = IN_PUML;
                } else {
                    /* copy line to output */
                    ouput_fn(output, optr, len);
                }
            } else if (status == IN_PUML) {
                /* maybe last char is not \n */
                if (judge_same(optr, len - (*ptr == '\n'), PUML_END, strlen(PUML_END))) {
                    /* run jar with stdin & copy jar's stdout buf to output */
                    ilen = optr - in;
                    ret = run_jar(jar_path, in, ilen, output, ouput_fn);
                    if (ret != 0) {
                        return ret;
                    }
                    status = NORMAL;
                }
            }
            optr = ptr + 1;
        }
        ptr++;
    }
    return 0;
}
// main.c
#include <stdlib.h>
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>

#include "puml.h"

struct dync_out {
    size_t osize;
    size_t olen;
    char *out;
};

void out_fn (void *output, const char *buf, size_t buflen)
{
    struct dync_out *out = (struct dync_out *)output;
    if (buflen + out->olen >= out->osize) {
        out->osize *= 2;
        char *tmp = realloc(out->out, out->osize);
        if (!tmp) {
            printf("vaild!!! realloc out failed\n");
            return;
        }
        out->out = tmp;
    }
    memcpy(out->out + out->olen, buf, buflen);
    out->olen += buflen;
}

int main (int argc, char *argv[])
{
    if (argc == 1) {
        return 1;
    }

    int fd = open(argv[1], O_RDONLY);

    size_t ilen = 0;
    size_t isize = 4096;
    char *in = malloc(isize);

    ssize_t n;

    while((n = read(fd, in, isize - ilen)) > 0) {
        if (ilen + n >= isize) {
            isize *= 2;
            char *tmp = realloc(in, isize);
            if (!tmp) {
                free(in);
                close(fd);
                printf("realloc failed!\n");
                return 1;
            }
            in = tmp;
        }
        ilen += n;
    }

    if (n < 0) {
        printf("read failed!\n");
        close(fd);
        free(in);
        return 1;
    }

    puml_input input = { in, ilen };

    struct dync_out out;
    out.osize = 4096;
    out.olen = 0;
    out.out = malloc(out.osize);

    int ret = puml2svg(&input, &out, out_fn);
    for (int i = 0; i < out.olen; i++) {
        putchar(out.out[i]);
    }

    close(fd);
    free(in);
    free(out.out);

    return ret;
}

写了大概一个下午,用C写字符串处理真是痛苦,感觉写了一天的realloc。缺少对C语言的经验总结,还是要多练多记。

另外,java真的好慢啊,严重拖慢了sapin的解析速度。