神马笔记实现共享方程公式

神马笔记实现共享方程公式

在前面的2个开发阶段,分别实现了『神马笔记』在文章模式和大纲模式下编辑方程公式元素。

这次开发的目标是实现共享方程公式,以实现知识分享。

一、目标

完成共享方程公式,以实现知识分享的目标。

二、共享方式

序号 方式 说明
1 拷贝到粘贴板 仅复制文字内容,包括段落、图片描述、方程描述、……
2 保存为图片 保存笔记截图,并添加笔名
3 保存为纯文本 与"拷贝到粘贴板"相同,将内容保存到文本文件
4 保存为Markdown 以markdown格式输出笔记元素
5 保存为Hexo Markdown 在Markdown的基础上增加hexo front-matter,
并按照hexo资源方式保存图片。
6 发送到第三方应用 同时发送笔记截图与文字内容,有第三方应用自行选择内容。

三、实现过程

1. 拷贝到粘贴板

依次将各种类型的DocumentEntity对象转化为ClipData.Item对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
List<ClipData.Item> list = document.getList().stream()
.map(e -> {
ClipData.Item item;

Class clz = e.getClass();
if (clz == ParagraphEntity.class) {
ParagraphEntity entity = (ParagraphEntity)e;

item = new ClipData.Item(entity.getText());
} else if (clz == PictureEntity.class) {
PictureEntity entity = (PictureEntity)e;

item = new ClipData.Item(entity.getText());
} else if (clz == FormulaEntity.class) {
FormulaEntity entity = (FormulaEntity)e;

StringBuilder sb = new StringBuilder();

// formula
if (!TextUtils.isEmpty(entity.getFormula())) {
sb.append(entity.getFormula());
} else {
sb.append(entity.getLatex());
}

// description
if (!TextUtils.isEmpty(entity.getText())) {
sb.append('\n');
sb.append(entity.getText());
}

item = new ClipData.Item(sb);
} else {
item = new ClipData.Item("");
}

return item;
})
.collect(Collectors.toList());

if/else的结构似乎应该优化一下了,采用类方式会更理想些。

2. 保存为图片

View绘制到离屏Bitmap并保存之。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Bitmap apply() {
Mode mode = chooseMode(view);
if (mode == null) {
return null;
}

Bitmap target = Bitmap.createBitmap(mode.mWidth, mode.mHeight, mode.mConfig);
Canvas canvas = new Canvas(target);
if (mode.mWidth != mode.mSourceWidth) {
float scale = 1.f * mode.mWidth / mode.mSourceWidth;
canvas.scale(scale, scale);
}
view.draw(canvas);

return target;
}

3. 保存为纯文本

拷贝到粘贴板类似,将各种类型的DocumentEntity对象添加到StringBuilder对象,然后保存之。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
StringBuilder sb = new StringBuilder(10 * 1024);

document.getList().stream()
.forEach(e -> {
Class clz = e.getClass();

if (clz == ParagraphEntity.class) {
ParagraphEntity entity = (ParagraphEntity)e;

sb.append(entity.getText());
} else if (clz == PictureEntity.class) {
PictureEntity entity = (PictureEntity)e;

sb.append(entity.getText());
} else if (clz == FormulaEntity.class) {
FormulaEntity entity = (FormulaEntity)e;

// formula
if (!TextUtils.isEmpty(entity.getFormula())) {
sb.append(entity.getFormula());
} else {
sb.append(entity.getLatex());
}

// description
if (!TextUtils.isEmpty(entity.getText())) {
sb.append('\n');
sb.append(entity.getText());
}
}


sb.append('\n');
});

4. 保存为Markdown

保存到纯文本类似,将各种类型的DocumentEntity转换为CharSequence对象,并保存之。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
Map<Class<? extends DocumentEntity>, Function<? super DocumentEntity, CharSequence>> createFactory(final boolean preview) {
HashMap<Class<? extends DocumentEntity>, Function<? super DocumentEntity, CharSequence>> map = new HashMap<>();

map.put(ParagraphEntity.class, e -> {
ParagraphEntity entity = (ParagraphEntity)e;
CharSequence text = entity.getText();
return text;
});

map.put(PictureEntity.class, e -> {
PictureEntity entity = (PictureEntity)e;
String name = pictureMap.get(entity);

StringBuilder sb = new StringBuilder();
sb.append(String.format("![%1$s]", entity.getText()));
sb.append(String.format("(%1$s)", name));

return sb;
});

map.put(FormulaEntity.class, e -> {
FormulaEntity entity = (FormulaEntity)e;

StringBuilder sb = new StringBuilder();

if (preview) {
sb.append("<p>");
}

String formula = entity.getFormula();
formula = (TextUtils.isEmpty(formula))? entity.getLatex(): formula;
if (MathMLTransformer.isMathML(formula)) {
sb.append(formula);
} else {
sb.append("$$\n");
sb.append(getLatex(formula, preview));
sb.append("\n$$");
}

if (preview) {
sb.append("</p>");
}

return sb;
});

return map;
}

5. 保存为Hexo Markdown

在markdown的基础上,添加hexo front-matter信息。

并按照hexo assets资源方式存储图片。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
String createFrontMatter(RecordEntity entity) {
StringBuilder sb = new StringBuilder(1024);
sb.append("---\n");

// uuid
{
sb.append("uuid: ");
sb.append(getUUID(entity));
sb.append('\n');
}

// title
{
sb.append("title: ");
sb.append(entity.getName());
sb.append('\n');
}

// categories
{
String value = getCategories(entity);
if (!TextUtils.isEmpty(value)) {
sb.append("categories: ");
sb.append(value);
sb.append('\n');
}
}

// tags
{
String value = getTags(entity);
if (!TextUtils.isEmpty(value)) {
sb.append("tags: ");
sb.append(value);
sb.append('\n');
}
}

// mathjax
if (hasFormula(document)) {
sb.append("mathjax: true\n");
}

// date
{
sb.append("date: ");
sb.append(getDate(entity));
sb.append('\n');
}

sb.append("---\n\n");


String text = sb.toString();
return text;
}

6. 发送到第三方应用

包含文本和截图2个内容。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
Intent intent = new Intent();
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);

boolean hasImage = (uri != null);
String text = getText(document, getFooter()).toString();

// mime-type
{
intent.setType("image/*");
intent.setAction(Intent.ACTION_SEND);
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION);

if (!hasImage) {
intent.setType("text/plain");
} else {
if (hasImage) {
hasImage = isSupport(context, resolveInfo, intent);
if (!hasImage) {
intent.setType("text/plain");
}
}
}
}

// component
{
String pkg = resolveInfo.activityInfo.packageName;
String cls = resolveInfo.activityInfo.name;
ComponentName cn = new ComponentName(pkg, cls);
intent.setComponent(cn);
}

// text
if (!TextUtils.isEmpty(text)) {
intent.putExtra(Intent.EXTRA_TEXT, text);
}

// picture
if (hasImage) {
if (intent.getAction().equalsIgnoreCase(Intent.ACTION_SEND)) {

intent.removeExtra(Intent.EXTRA_STREAM);
intent.putExtra(Intent.EXTRA_STREAM, uri);
}

if (intent.getAction().equalsIgnoreCase(Intent.ACTION_SEND_MULTIPLE)) {

ArrayList<Uri> imageUris = new ArrayList();
imageUris.add(uri); // Add your image URIs here

intent.removeExtra(Intent.EXTRA_STREAM);
intent.putParcelableArrayListExtra(Intent.EXTRA_STREAM, imageUris);
}
}

// start
try {
context.startActivity(intent);
} catch (Exception e) {

}

四、预览Markdown

1. 使用HTML预览

在6中分享方式中,2种Markdown方式无法直接根据输入内容进行预览。

Android平台并没有内置支持Markdown的应用,无法直接预览。

因此需要转换为HTML格式以实现预览。

序号 方式 预览
1 拷贝到粘贴板 与正文一致
2 保存为图片 与正文一致
3 保存为纯文本 与正文一致
4 保存为Markdown 使用HTML进行预览
5 保存为Hexo Markdown 使用HTML进行预览
6 发送到第三方应用 与正文一致

2. 遇到的问题

在添加方程公式元素之前,一切工作良好。

添加了方程公式元素之后,会发现很多方程公式无法正常预览。

原因在于markedmathjax发生了冲突。

模块 说明
marked 将markdown文档渲染为html文档
mathjax 渲染html文档中的方程公式,支持latex及mathml

在marked渲染markdown文档时,对一些特殊字符进行了转义,同是将latex中的一些字符进行了转义。从而导致latex公式遭到破坏,进而无法正常显示公式。

详细冲突细节,请参考一下文章。

  1. 在Hexo中渲染MathJax数学公式
  2. 【Markdown】Markdown 使用MathJax引擎 书写Latex 数学公式
  3. Hexo下mathjax的转义问题
  4. 让marked与MathJax和谐共存

3. 解决方案

文章中给出了2种解决冲突的方案。

方案 问题
修改marked 使用在线marked,无法修改
修改markdown渲染引擎为pandoc 需要另外安装pandoc进行渲染

因此已有的2种方案都不适合『神马笔记』。

最终的解决方案将LaTex或MathML包含在<p></p>之间。

因为marked不会对内嵌的html代码进行转义,这样保证了原始的内容。

4. 新的问题

将包含<p></p>的方程公式保存为markdown后。

使用Typora无法直接预览方程公式,因为被处理成内嵌的HTML代码。

因此需要分成2份Markdown进行输出。

Markdown输出类型 处理方式
正文 方程公式不包含<p></p>
预览 将方程公式包含在<p></p>

5. 再次遇到问题

使用『神马笔记』导出markdown后,使用Hexo引擎发布到个人博客时,发现很多公式不能正常显示。

因为Hexo使用marked渲染markdown文件,导致LaTeX被错误转义。

修改的方式有2种。

序号 方案 参考资料
1 修改marked Hexo下mathjax的转义问题
2 修改markdown渲染引擎为pandoc Hexo下mathjax的转义问题

最终选择pandoc方式。

比较修改引擎的方式,更偏向于替换整个引擎。

五、接下来

神马笔记』在笔记中插入方程公式元素功能开发到此结束。

接下来2件事情要做:

  1. 编写LaTex数学公式简明手册
  2. 发布新版本『神马笔记

六、Finally

~燕雁无心~太湖西畔随云去~