基于注解的简单权限控制实现


前情提要

这个其实是很早的时候在做本科毕设的时候实现的, 当时因为权限结构简单, 懒得上一个类似于Spring Security这么重的框架, 以及主要是想尝试一下自己实现注解就简单做了一下基于方法注解的权限控制.

设计思路

因为整个系统只有管理员/登录用户/未登录用户三种不同的角色, 并同时考虑到有些接口(比如修改/删除数据)只有作者才能访问, 所以一共设计了@AdminOnly, @CreatorOnly@LoginOnly三种不同的注解.

注解部分实现

注解的实现代码很简单, 主要是注意需要标记@Retention(RetentionPolicy.RUNTIME)才能在运行时通过反射访问到这个注解:

package weplay.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Created by sl on 17/4/18.
 * check the accessor is admin
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface AdminOnly {
    /**
     * exclude method's name when mark class
     */
    String[] exclude() default {};
    /**
     * indicate the check is valid
     */
    boolean isValid() default true;
}

因为需要@AdminOnly的方法很多, 为了简单所以除了方法注解以外也设置了类注解, 添加exclude属性以排除某一个类中不需要使用该注解过滤的方法, isValid是为了临时禁用该注解, 方便调试.

package weplay.annotation;

import weplay.enums.EntityType;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Created by sl on 17/4/18.
 * check the accessor is creator
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface CreatorOnly {
    /**
     * indicate EntityType of Entity
     */
    EntityType type();

    /**
     * indicate the position of EntityId in URI
     */
    int position();

    /**
     * indicate the check is valid
     */
    boolean isValid() default true;

    /**
     * indicate admin can access this handler
     */
    boolean allowAdmin() default true;
}

这里的设计比较纠结, 因为我需要验证当前用户对当前访问的数据条目有没有权限, 所以需要使用type来获得访问数据条目的业务类型, 以及需要通过position来知道具体的数据条目的id, 这样我才能查询到当前访问的数据条目的创建者, 从而判断是否允许访问, allowAdmin表示除了创建者管理员是否也能访问该接口.

package weplay.annotation;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * Created by sl on 17/4/22.
 */
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD})
public @interface LoginOnly {
    /**
     * indicate the check is valid
     */
    boolean isValid() default true;
}

这个表示是否只有登录用户才能访问该接口.

注解解析实现

注解的解析放在SpringMVC的Interceptor中, 因为HandlerInterceptor是单例需要可重入, 所以使用了HandlerInterceptor封装RequestContextHolder.currentRequestAttributes()在当前请求的上下文中临时保存值.
fetchInfo()用于从请求中读取后续解析需要的数据, processAdminOnly(), processCreateOnly()processLoginOnly()分别解析三个不同的注解, 这里可以考虑做一个公共接口, 然后不同的注解做不同的实现.
preHandle()中, 通过handlerMethod.getBeanType().getAnnotations()handlerMethod.getMethod().getAnnotations()分别获取类注解和方法注解, 这里使用了Spring提供的HandlerMethod类, 如果直接使用反射的话, 可以通过class.getAnnotations()class.getMethod(name, parameterTypes).getAnnotations()分别获取到类注解和方法注解.
获取到注解对象以后, 可以通过直接之前定义的方法来获取到注解配置时设置的值, 比如adminOnly.exclude(), 然后根据业务需要判断是否放行还是抛出异常中止请求响应.

package weplay.aop;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import weplay.annotation.AdminOnly;
import weplay.annotation.CreatorOnly;
import weplay.annotation.LoginOnly;
import weplay.enums.AccountState;
import weplay.enums.AccountType;
import weplay.enums.EntityType;
import weplay.enums.ErrorCode;
import weplay.exception.CustomException;
import weplay.helperBean.viewBean.UserInfo;
import weplay.resource.Settings;
import weplay.service.IActivityService;
import weplay.service.IAuthService;
import weplay.service.IEntityService;
import weplay.service.IUserService;
import weplay.utils.RequestContextUtil;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.lang.annotation.Annotation;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

/**
 * Created by sl on 17/4/18.
 */
public class AuthorityInterceptor implements HandlerInterceptor {
    private IAuthService authService;
    private IUserService userService;
    private IActivityService activityService;
    private IEntityService entityService;

    /**
     * fetch basic info from request
     */
    private void fetchInfo(HttpServletRequest req) throws CustomException {
        String uri = req.getRequestURI();
        String token = req.getHeader("token");
        Integer currentUid;
        AccountType accountType;
        UserInfo userInfo;
        if (token != null) {
            currentUid = authService.readUidByToken(token);
            if (currentUid == Settings.ROOT_ADMIN_ID) {
                accountType = AccountType.ADMIN;
            } else {
                accountType = AccountType.NORMAL;
            }
            userInfo = userService.readUserInfoByUid(currentUid);
        } else {
            token = "";
            currentUid = -1;
            accountType = AccountType.TOURISM;
            userInfo = new UserInfo();
        }
        // save to RequestContextUtil
        RequestContextUtil.setAttribute("uri", uri);
        RequestContextUtil.setAttribute("token", token);
        RequestContextUtil.setAttribute("uid", currentUid);
        RequestContextUtil.setAttribute("accountType", accountType);
        RequestContextUtil.setAttribute("userInfo", userInfo);
    }

    /**
     * process @AdminOnly
     */
    private void processAdminOnly(Annotation annotation, HandlerMethod handlerMethod) throws CustomException {
        AccountType accountType = (AccountType) RequestContextUtil.getAttribute("accountType");

        if (annotation instanceof weplay.annotation.AdminOnly) {
            AdminOnly adminOnly = (AdminOnly) annotation;
            // check isValid
            if (!adminOnly.isValid()) {
                return;
            }
            // check exclude method
            for (String s : adminOnly.exclude()) {
                if (s.equals(handlerMethod.getMethod().getName())) {
                    return;
                }
            }
            // check current user is admin
            if (accountType != AccountType.ADMIN) {
                throw new CustomException(ErrorCode.NO_PERMISSION, "ADMIN ONLY");
            }
        }
    }

    /**
     * process @CreateOnly
     */
    private void processCreateOnly(Annotation annotation) throws CustomException {
        String uri = (String) RequestContextUtil.getAttribute("uri");
        Integer currentUid = (Integer) RequestContextUtil.getAttribute("uid");
        AccountType accountType = (AccountType) RequestContextUtil.getAttribute("accountType");

        if (annotation instanceof CreatorOnly) {
            CreatorOnly creatorOnly = (CreatorOnly) annotation;

            // read EntityType and EntityId
            String[] uriPartition = uri.split("/");
            String entityIdStr = uriPartition[creatorOnly.position()];
            Integer entityId;
            if (entityIdStr.contains(".")) {
                entityId = Integer.valueOf(entityIdStr.split("\\.")[0]);
            } else {
                entityId = Integer.valueOf(entityIdStr);
            }
            EntityType entityType = creatorOnly.type();

            // read CreatorId
            Integer creatorId = -1;
            switch (entityType) {
                case ACTIVITY:
                    creatorId = activityService.readActivityById(entityId).getCreator();
                    break;
                case NOTIFICATION:
                    creatorId = entityService.readNotificationById(entityId).getCreator();
                    break;
                case PHOTO:
                    creatorId = entityService.readPhotoById(entityId).getCreator();
                    break;
            }

            // update accountType
            if (currentUid.equals(creatorId)) {
                accountType = AccountType.CREATOR;
                RequestContextUtil.setAttribute("accountType", accountType);
            }

            // check current User is creator or admin if admin allow access
            if (creatorOnly.isValid() && accountType != AccountType.CREATOR &&
                    !(creatorOnly.allowAdmin() && accountType == AccountType.ADMIN)) {
                throw new CustomException(ErrorCode.NO_PERMISSION, "CREATOR ONLY");
            }
        }
    }

    /**
     * process @CreateOnly
     */
    private void processLoginOnly(Annotation annotation) throws CustomException {
        AccountType accountType = (AccountType) RequestContextUtil.getAttribute("accountType");
        UserInfo userInfo = (UserInfo) RequestContextUtil.getAttribute("userInfo");

        if (annotation instanceof LoginOnly) {
            LoginOnly loginOnly = (LoginOnly) annotation;
            
            // check isValid
            if (!loginOnly.isValid()) {
                return;
            }
            if (accountType == AccountType.TOURISM) {
                throw new CustomException(ErrorCode.NO_PERMISSION, "LOGIN USER ONLY");
            }
        }
    }

    public boolean preHandle(HttpServletRequest req, HttpServletResponse res, Object o) throws Exception {
        // static resource handler
        if (!(o instanceof HandlerMethod)) {
            return true;
        }
        // convert handler into current Type
        HandlerMethod handlerMethod = (HandlerMethod) o;
        // fetch basic info
        this.fetchInfo(req);
        // process Annotations
        List<Annotation> annotations = new ArrayList<Annotation>();
        annotations.addAll(Arrays.asList(handlerMethod.getBeanType().getAnnotations()));
        annotations.addAll(Arrays.asList(handlerMethod.getMethod().getAnnotations()));
        for (Annotation a : annotations) {
            this.processAdminOnly(a, handlerMethod);
            this.processCreateOnly(a);
            this.processLoginOnly(a);
        }
        return true;
    }
}
package weplay.utils;

import org.springframework.web.context.request.RequestAttributes;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * Created by sl on 17/4/22.
 * Utils for wrapping RequestContextHolder
 */
public class RequestContextUtil {
    /**
     * prefix for user custom Attribute
     */
    private static String prefix = "u_";

    /**
     * set Attribute to Context
     */
    public static void setAttribute(String key, Object obj) {
        RequestContextHolder.currentRequestAttributes().setAttribute(prefix + key, obj, RequestAttributes.SCOPE_REQUEST);
    }

    /**
     * get Attribute from Context
     */
    public static Object getAttribute(String key) {
        return RequestContextHolder.currentRequestAttributes().getAttribute(prefix + key, RequestAttributes.SCOPE_REQUEST);
    }

    /**
     * remove Attribute at Context
     */
    public static void removeAttribute(String key) {
        RequestContextHolder.currentRequestAttributes().removeAttribute(prefix + key, RequestAttributes.SCOPE_REQUEST);
    }

    /**
     * get Current Request
     */
    public HttpServletRequest currentRequest() {
        return ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
    }

    /**
     * get Current Response
     */
    public HttpServletResponse currentResponse() {
        return ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getResponse();
    }
}

注解的使用

配置好注解类并在拦截器中处理注解以后, 那么只需要在相应的地方进行注解标注就可以了, 如同使用Java自带或Spring提供的注解一样. 例如:

@RestController
@AdminOnly(exclude = {"createReport"})
public class AdminController {
    \\...
}
@CreatorOnly(type = EntityType.NOTIFICATION, position = 2)
@RequestMapping(path = "/notification/{id:[0-9]+}", method = RequestMethod.DELETE)
public ResponseEntity<String> deleteNotification(@PathVariable("id") Integer id) throws CustomException {
    \\...
}

总结与回顾

总的来说, 注解本质上相当于只是给类或者方法打了一个标记, 具体想要知道一个类或者一个方法是否有这个标记需要通过反射来获得, 并进行对应的处理.
通过反射对于类进行操作, 总体来说是比较慢的, 目前各大框架的配置逐渐从XML形式转变为注解形式, 这一过程是否会造成比较大的性能损失是一个需要思考的问题.
在这一次的例子中, 每一次请求都对其所有的注解进行遍历是比较慢而且进行了很多操作, 可以考虑增加一个HashMap用于记录一个类/方法对应了哪几个注解, 这样下次访问同一个类/方法时直接查表就可以而不用再通过反射来获取. 或者考虑倒排索引的形式, 记录每一个注解都对应哪些方法, 从而避免重复的查询和处理.


简明ArchLinux安装教程


概述

自用的简明ArchLinux安装教程, 主要参考官方Guide, 部分组件按照自己的喜好来

  • 引导管理采用grub2
  • 网络管理采用systemd-networkd
  • 文件系统用的btrfs

启动ArchISO

略 (这都不会还是立刻右上角吧

网络配置

ArchLinux的安装需要网络, 如果是DHCP的动态网络应该默认就配置好了.

如果是静态配置, 那么将需要手动进行配置, 下面将使用systemd-networkd作为网络管理器, 网络配置采用Arch官方推荐的iproute2而不是经典的net-tools.

动态网络配置

'/etc/systemd/network/$interface.network'

[Match]
Name=$interface

[Network]
DHCP=yes
systemctl start systemd-networkd.service systemd-resolved.service

静态网络配置

确认网卡名称并启动网卡(如果没有启动的话)

ip addr
ip link set $interface up

配置systemd-networkd

'/etc/systemd/network/$interface.network'

[Match]
Name=$interface

[Network]
Address=$address/$prefix
Gateway=$gateway
DNS=$dns_server
systemctl start systemd-networkd.service systemd-resolved.service

手动配置静态IP, 路由和DNS

ip address add $address/$prefix broadcast + dev $interface
ip route add default via $gateway dev $interface
echo 'nameserver $dns_server' >> /etc/resolv.conf

检测网络是否联通

ping ip.cn

时间和地区设置

设置正确的时区

timedatectl set-timezone Asia/Shanghai

启用NTP服务(TLS连接需要正确的时间)

timedatectl set-ntp true
timedatectl status

设置正确的Locales

locale-gen
locale | sed 's/=.*/="en_US.UTF-8"/g' > /etc/locale.conf

磁盘分区

  • 确定DiskName
fdisk -l
  • 磁盘分区

我一般习惯会分一个swap, 因为使用btrfs所以还要再分一个根卷/, 如果使用EFI则需要再分一个 /efi.

fdisk $disk
o: create DOS partition table
g: create GPT partition table
n: add a new partition
d: delete a partition
t: change a partition type
p: print the partition table
w: write table to disk and exit
如果使用的是传统BIOS的话, 需要使用t命令将启动分区的类型设置为BIOS boot(4).
  • 格式化分区
mkfs.ext4 $partition # /
mkswap $partition && swapon $partition # swap
mkfs.btrfs -L $label $partition # / (btrfs)
mkfs.vfat $partition # /efi
  • 挂载分区
mount $partition /mnt
mkdir /mnt/efi && mount $partition /mnt/efi # EFI分区

解压基本系统

  • 修改软件源

/etc/pacman.d/mirrorlist

  • 解压基本系统和安装基本包
pacstrap /mnt grub base linux linux-firmware btrfs-progs sudo openssh 
pacstrap /mnt vim wget zsh git # 其他的一些基本包
  • 生成fstab
genfstab -U /mnt >> /mnt/etc/fstab
  • 复制网络配置
cp /etc/systemd/network/$interface.network /mnt/etc/systemd/network/$interface.network
  • Chroot
arch-chroot /mnt

配置基本系统

  • 配置网络并默认启动SSH
systemctl enable systemd-networkd.service
systemctl enable systemd-resolved.service
systemctl enable sshd.service
  • 配置Root密码
passwd
  • 添加新用户并配置Sudo
useradd -m -G root $user && passwd $user
cat '%sudoers ALL=(ALL) ALL' >> /etc/sudoers
  • 生成Initramfs
mkinitcpio -P
  • 配置grub
grub-install --target=i386-pc /dev/sdX
grub-mkconfig -o /boot/grub/grub.cfg
至此, 重启就可以进入到Arch的系统里了

额外的系统配置

  • 设置时区和硬件时钟
ln -sf /usr/share/zoneinfo/$Region/$City /etc/localtime
hwclock --systohc
  • 配置Locale

/etc/locale.gen

locale-gen
locale | sed 's/=.*/="en_US.UTF-8"/g' > /etc/locale.conf
  • 配置主机名和Hosts
echo $hostname > /etc/hostname

/etc/hosts

127.0.0.1   localhost
::1     localhost

尾声

这样就配置好了ArchLinux的基本系统, 其他的图形配置可以重启以后在SSH里慢慢配置.


MTJ-N + Intel MKL的配置和使用


前情提要

众所周知, 在python的世界, 矩阵库最好用的莫过于numpy, 类似于Matlab的语法, 让很多人从Matlab迁移过来毫无压力.

然而, 在Java的世界, 虽然以Spring为主的Web框架非常强大, 但是科学计算库的支持相比之下就相当感人. 倒并不是缺乏支持, 而是你很难找到一个足够好用效率又高支持又全的矩阵库.

当然实际上, 对于矩阵库的使用还是以效率为主, 所以今天要介绍的是Java世界中效率最高的矩阵库之一matrix-toolkits-java, 虽然作者已经不再维护, 但是性能经过实测依然吊打其他所有.

这个库性能高的主要原因在于它使用了Native库, 相比同样使用Native库的JBLAS, 这个库因为能够支持Intel MKL和Nvidia CUDA, 使其性能更高, 相关的Java矩阵库比较可以在Java Matrix Benchmark这里看到详细的比较.

正片开始

MTJ配置使用

MTJ在Maven中心仓库里, 所以如果使用Maven来管理依赖的话, 那么只要简单的添加即可:

<dependency>
    <groupId>io.github.andreas-solti.matrix-toolkits-java</groupId>
    <artifactId>mtj</artifactId>
    <version>1.0.7</version>
</dependency>

这里用的是另外一个Fork, 似乎是修正了原作者版本的一些问题, 不过没仔细看. 关于MTJ的文档可以参考这个: http://www.javadoc.io/doc/io.github.andreas-solti.matrix-toolkits-java/mtj/1.0.7.

使用这个库务必需要注意的是很多操作都是不复制而是在原矩阵上修改的, 例如:

public Matrix add(double alpha, Matrix B) {
    checkSize(B);

    if (alpha != 0)
        for (MatrixEntry e : B)
            add(e.row(), e.column(), alpha * e.get());

    return this;
}

public void add(int row, int column, double value) {
    set(row, column, value + get(row, column));
}

所以使用时如果要符合一般的使用习惯请务必注意:

/**
 * Simplify Some DenseMatrix Operation
 */
private static class DenseMatrixUtil{
    // return C=A+B
    public static DenseMatrix add(DenseMatrix A, DenseMatrix B){
        return (DenseMatrix)A.copy().add(B);
    }

    // return A=A+B
    public static DenseMatrix addi(DenseMatrix A, DenseMatrix B){
        return (DenseMatrix)A.add(B);
    }

    // return C=A-B
    public static DenseMatrix sub(DenseMatrix A, DenseMatrix B){
        return (DenseMatrix)A.copy().add(-1, B);
    }

    // return A=A-B
    public static DenseMatrix subi(DenseMatrix A, DenseMatrix B){
        return (DenseMatrix)A.add(-1, B);
    }

    // return C=s*A
    public static DenseMatrix mul(DenseMatrix A, double s){
        return (DenseMatrix)A.copy().scale(s);
    }

    // return A=s*A
    public static DenseMatrix muli(DenseMatrix A, double s){
        return (DenseMatrix)A.scale(s);
    }

    // return C=A*B
    private static DenseMatrix mmul(DenseMatrix A, DenseMatrix B){
        DenseMatrix C = new DenseMatrix(A.numRows(), B.numColumns()); // C.rows = A.rows, C.cols = B.cols
        return (DenseMatrix)A.mult(B, C);
    }

    // Compute \sum{alpha[i] * matrix[i]}
    private static DenseMatrix linear(double[] alphas, DenseMatrix[] matrixs){
        if(alphas.length != matrixs.length){
            throw new IllegalArgumentException("alpha's length should equal matrix's length");
        }

        DenseMatrix result = (DenseMatrix)new DenseMatrix(matrixs[0].numRows(), matrixs[0].numColumns()).zero();
        for(int i = 0; i < alphas.length; i++){
            result.add(alphas[i], matrixs[i]); // result = alpha * matrix + result
        }
        return result;
    }
}

与Intel MKL整合

Intel MKL全称是Intel® Math Kernel Library, 是Intel为其处理器设计的一套高度优化的Native矩阵与线性运算函数库, 相比OpenBLAS有着高得多的性能. 至于AMD处理器能不能使用, 这个我就不知道了, 但是我还是要喊出 AMD YES! (逃

Intel MKL安装

在Linux下, 只要直接从源里直接安装即可, 不同发行版不一样搜索一下即可, 以ArchLinux为例:

pacman -S intel-mkl

Arch的官方源里是没有MKL的, 如果需要的话, 请使用ArchlinuxCN或者Arch4Edu源.

在Windows下, 只能从官网注册开发者然后申请, 几天之后会给你发邮件然后提供下载连接下载Windows版的安装包.
这个安装包是包含devel部分的开发包, 事实上我们只需要redist部分即可, 为了方便使用我打包了2018.3.210的redist包(x86/x64). 事实上, Anaconda里面也是带Intel MKL Redist的, 完全可以从里面扒出来...

Intel MKL配置

在安装好Intel MKL以后还需要做两件事情:

  1. mkl\mkl_rt.dll软链接或者复制为libblas3.dllliblapack3.dll
  2. mklcompiler两个文件夹添加到系统的PATH环境变量中

这样, 才能够被MTJ-N所识别并使用.

如果不使用Intel MKL, MTJ似乎会自动使用OpenBLAS的实现, 其输出是下面这样的:

一月 14, 2019 11:31:15 上午 com.github.fommil.netlib.BLAS <clinit>
警告: Failed to load implementation from: com.github.fommil.netlib.NativeSystemBLAS
一月 14, 2019 11:31:15 上午 com.github.fommil.jni.JniLoader liberalLoad
信息: successfully loaded C:\Users\$USER\AppData\Local\Temp\jniloader7616017108689444760netlib-native_ref-win-x86_64.dll
一月 14, 2019 11:31:33 上午 com.github.fommil.netlib.LAPACK <clinit>
警告: Failed to load implementation from: com.github.fommil.netlib.NativeSystemLAPACK
一月 14, 2019 11:31:33 上午 com.github.fommil.jni.JniLoader load
信息: already loaded netlib-native_ref-win-x86_64.dll

性能虽然会比纯Java实现要好, 但是与MKL相比之下就要逊色很多了.

如果正确识别到MKL的话, 应该会类似的输出:

一月 14, 2019 11:38:25 上午 com.github.fommil.jni.JniLoader liberalLoad
信息: successfully loaded C:\Users\$USER\AppData\Local\Temp\jniloader3275505562389233726netlib-native_system-win-x86_64.dll
一月 14, 2019 11:38:26 上午 com.github.fommil.jni.JniLoader load
信息: already loaded netlib-native_system-win-x86_64.dll

微不足道的总结

如果要做科学计算方面的工作的话, 还是:

人生苦短, 快用Python (并不

给博客增加MathJax支持


MathJax是一个非常著名的用于在网页中显示公式的JS库, 主要是你用类似于>$\LaTeX$<的语法写下公式他就会自动帮你渲染非常方便. 比如:

$$ E=mc^2 $$

或者更加复杂一点:

$$ \normalsize \varepsilon=\sum_{i=1}^{n-1} \frac1{\Delta x}\int\limits_{x_i}^{x_{i+1}}\left\{\frac1{\Delta x}\big[ (x_{i+1}-x)y_i^\ast+(x-x_i)y_{i+1}^\ast\big]-f(x)\right\}^2dx $$

以前我也弄过这个, 不过用的是第三方的插件, 然后和Markdown混合渲染问题也很多, 最近发现直接在footer.php里添加脚本来加载就好了. 地址用的是一个国内的CDN, 有空再把地址配置到服务器上去把Orz

然后我把行内触发的标签从$$改成了>$$<, 因为两个美元符号还是蛮容易误触发的...

<script type="text/x-mathjax-config">
    MathJax.Hub.Config({
        extensions: ["tex2jax.js"],
        jax: ["input/TeX", "output/HTML-CSS"],
        tex2jax: {
            inlineMath: [ ['>$','$<']],
            displayMath: [ ['$$','$$']],
            processEscapes: true
        },
        "HTML-CSS": { fonts: ["TeX"] }
    });
</script>
<script src='https://cdn.bootcss.com/mathjax/2.7.5/latest.js?config=TeX-MML-AM_CHTML' async></script>

顺便推荐一个MathTeX的参考网站参考书籍, 非常好使


如何将Markdown转换为MediaWiki


起因

虽然Markdown不是完美的标记语言, 或多或少有一些不足, 但是MediaWiki是真的难用! 所以如何将Markdown写好的文档转换为MediaWiki就变成了一个问题.

根据调研发现了Pandoc这样一个工具可以在任意两种文档之间进行转换, 具体支持哪些可以去官方的介绍界面去看, 当然我们只要知道它可以做到Markdown2MediaWiki的转换就好了.

这种转换经过测试, 基本上还可以, 就是代码块的转换上会有一点小问题, 不过这个会在后文给出正则表达式进行替换一下就好了.

使用方法

  1. 安装pandoc: https://pandoc.org/installing.html

    • WSL用户可以直接在WSL里使用包管理器安装源里的版本, 简单省事
  2. 打开终端(CMD)切换目录到源文件目录, 然后运行pandoc markdown.md -f markdown -t mediawiki -o mediawiki.wiki就可以完成文档的转换

    • Markdown的源文件是markdown.md, 输出文件是mediawiki.wiki, 当然可以自定义

一些问题

对代码块的处理有问题

pandonc默认是将```的代码块标记转换为HTML的<code>标签, 但是MediaWiki是不支持这个标签的, 因此需要将其转换为<pre>标签.

示例正则表达式如下:

Find: (</*)source([^>]*>)
Replace: \1pre\2

在Sublime下测试过没问题, 如果使用其正则表达式引擎特别是sed请自行测试修改.

总结

MediaWiki真的难用啊啊啊啊锕啊啊