搭建我的笔记系统

目录

本站点就是这样构建的

搭建完成后的快捷键 描述
C-c en 把 org 格式的笔记导出为 html 格式的笔记,从 ~/notes/org/ 导出到 ~/notes/html/ 。该命令导出的笔记在本地测试环境使用
C-c eN 该命令导出的笔记在生产环境使用。(en 为 export notes 的首字母)

1 为什么选用 org-mode 构建?

之前一直使用 evernote + gitbook 等工具记笔记。然而每个都有各自很大的缺点

  • evernote 其它的都好,就是文本编辑器太过垃圾,用的越久越无法忍受。
  • gitbook 是基于 markdown 的,它的每篇文档都是一个 markdown 文件。但 markdown 过于轻量,不支持复杂格式。

而 org-mode 可以很好的满足我的需求

  • 表格功能、超链接功能强大
  • 支持 latex
  • 基于大纲
  • 支持导出各种格式。例如:配置后可以导出成一个博客网站,能在各种终端上用浏览器直接访问。
  • 可以根据自己的想法编写 CSS、js 和 HTML 定制笔记系统的外观
  • 导出成网页后,只需一个链接就可以共享给别人

搭建笔记系统的几个基本要求或目标:

  • 要有一个导航栏
  • 主页存放经常访问的笔记。如 shell 命令、快捷键等
  • 对笔记进行分类。比如:shell 笔记、算法笔记
  • 采用 google 搜索
  • 目录浮于右上角,点击隐藏,再点击显示;目录聚焦当前大纲。
  • 简单美观。支持代码高亮

2 笔记系统最终目录结构

$ tree -N -I "algorithm|libevent|test" notes
notes                            # 搭建的笔记系统目录
├── html                         # 导出成 h5 的笔记本
└── org                          # org 目录
    ├── css                      # 存放 css 样式,js 文件
    │   ├── main.js              # 自定义的 js 文件
    │   ├── jquery-2.1.3.min.js  # jquery
    │   └── style.css
    ├── gallery.org              # 画廊
    ├── images                   # 所有的图片都保存在这里
    ├── index.org                # 主页,可以保存一些经常需要参考的笔记的链接,从而能快速访问
    ├── links.org                # 优秀的博客链接
    ├── notebooks.org            # 创建的所有笔记本都在这里
    ├── emacs notebook           # 存放 emacs 笔记的笔记本
    │   └── emacs.org            # 笔记目录
    ├── libevent notebook        # 笔记本
    │   └── libevent.org         # 笔记目录
    ├── shell notebook
    │   └── shell.org
    │── templates                # html 模版目录。导出 html 时会根据相关配置把这些模版放到 html 的合适位置。生产环境使用该目录n
    │   ├── html-head.html       # html head
    │   ├── postamble.html       # 放到 html 的尾部
    │   └── preamble.html        # 放到 html 的开始部分
    └── templates-test           # 测试环境使用这个目录而不用 templates
        ├── html-head.html
        ├── postamble.html
        └── preamble.html

3 org-mode 导出成网站的基本配置

我的 emacs org-mode 的配置文件是 init-org.el ,向该文件添加如下配置

(require 'org)
(require 'ox-html)
(require 'ox-publish)
(require 'htmlize)

(setq org-export-with-entities t)   ;; 导出时是否进行转义。查看转义字符命令:M-x org-entities-help。例如:将 org 文档中的 \vbar 转义成 html 中的 |

;; HTML模板目录
(defvar *site-template-directory* "~/notes/org/templates")

(setq org-publish-project-alist
      '(
        ("org-notes"  ;; org 组件。主要是把 org 根据下列的配置规则转换成 h5 放到目标文件夹内
         :base-directory "~/notes/org"
         :base-extension "org"
         :publishing-directory "~/notes/html"
         :recursive t
         :publishing-function org-html-publish-to-html    ;; 发布的方式。这里是 org 转换成 html
         :headline-levels 4
         :language "zh-CN"              ;; 设置为 zh-CN 会影响一些东西。比如:目录会显示为汉字
         :section-numbers t             ;; 是否为标题编号
         :with-toc t                    ;; 是否创建 table of contents
         :with-latex t                  ;; 是否可以使用 latex
         :html-doctype "html5"          ;; 导出 h5
         :with-sub-superscript {}       ;; 禁用 _ 转义成下标,^转义成上标。但加 {} 就可以转义了
         :author "XXX"
         :email "XXX"
         :preserve-breaks t             ;; 是否保留换行符。如果设置为 nil,导出后就会多行文本显示在一行
         :html-head-include-default-style nil  ;; 取消默认的 css
         :html-head-include-scripts nil        ;; 取消默认的 javascript 代码
         :exclude "test*\\|.*\.test\.org"      ;; test 为前缀的文件和文件夹都不导出 html
         :include ("./test/math.org" "./test/worg.org" "./test/o-blog.org")          ;; 虽然 math.org 在 test 文件夹里,但依然会导出到 html,显然 include 比 exclude 优先
         )
        ("static"   ;; 静态组件,表示这些文件原封不动的拷贝到目标文件夹
         :base-directory "~/notes/org"
         :base-extension "css\\|js\\|png\\|jpg\\|gif\\|pdf\\|mp3\\|ogg\\|swf\\|txt\\|asc\\|ico\\|tar"
         :publishing-directory "~/notes/html"
         :recursive t
         :publishing-function org-publish-attachment) ;; 发布方式。原封不动的拷贝
        ("notes" :components ("org-notes" "static"))  ;; 笔记本发布组件。注意:~/notes/org 目录中的文件或文件夹名称如果带有 "notes",就不会发布该文件夹或 org 文件。暂未找到原因
        ))

;; css 文件如果修改了,就需要重新加载该 el 文件,这样才能看到 html 样式的变化
;; html-head.html 文件用来设置 html 的 <head> 部分。该文件中引入了 CSS 文件
;; preamble.html 文件包含导航栏 html、谷歌搜索
;; postamble.html 文件包含了网站声明、引入了 js 文件
(setq org-html-head (read-html-template "html-head.html"))
(setq org-html-preamble (read-html-template "preamble.html"))
(setq org-html-postamble (read-html-template "postamble.html"))

;;; 设置Mathjax库的路径
(add-to-list 'org-html-mathjax-options '(path "https://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML"))

(provide 'init-org)

我的 emacs 自定义函数的配置文件是 init-func.el ,向其中添加用于读取 notes/templates/ 目录下的文件的函数和导出笔记本的函数。

(defun read-html-template (template-file)
  (with-temp-buffer
    (insert-file-contents (concat *site-template-directory* "/" template-file))
    (buffer-string)))

;; 该函数会强制导出所有文件,即使该文件没有修改过。否则,可能出现有些文件修改了,但是导出的还是旧文件
(defun export-my-notes()
  (interactive)
  ;; solarized 配色问题。需要设置为 256 色。否则,在终端下的 emacs 中执行该函数,导出的代码块颜色混乱
  (setq solarized-termcolors 256)
  (load-theme 'solarized t)

  (org-publish-project "notes" t)  ;; 导出 notes 到 html。t 表示强制导出

  ;; 导出完毕后,配色再改回来,防止 solarized 在终端中颜色混乱
  (setq solarized-termcolors 16)
  (load-theme 'solarized t)
  )

我的 emacs 快捷键配置文件是 init-keybindings.el ,向其中添加快捷键

(global-set-key (kbd "C-c e n") 'export-my-notes)

写完笔记后,就可以使用快捷键 C-c en 直接把笔记发布到 notes/html 目录。
再通过 git 命令进行版本控制,在网站主机上拉取网站仓库就可以在网上访问笔记了。

4 后续优化1 —— 代码块右侧显示代码类型

如果代码块是 shell 脚本,代码块右上角显示 shell;如果是 python 代码,右上角显示 python;…

网页的 CSS 配置如下
主要借助 :before 插入语言名称

pre {
    background-color: #fff;
    border: #ddd solid 1px;
    border-radius: 10px;
    box-shadow: 3px 3px 3px #eee;
    color: Black;
    font: 14px/1.5 monospace;
    padding: 10px;
}

pre.example {
    white-space: pre-wrap;
}

pre.src {
    position: relative;
    overflow-x: auto;
}

pre.src:before {
    display: inline;
    position: absolute;
    font-size: 17px;
    font-weight: bold;
    text-shadow: 0 0 0 rgba(0, 0, 0, 0.2);
    color: #bfbfbf;
    top: 5px;
    right: 10px;
    padding: 3px;
}

/* Languages per Org manual */
pre.src-awk:before { content: 'awk'; }
pre.src-C:before { content: 'C'; }
pre.src-c:before { content: 'C'; }
pre.src-css:before { content: 'CSS'; }
pre.src-emacs-lisp:before { content: 'Emacs Lisp'; }
/* ... */

5 后续优化2 —— 生产环境测试环境分离

需要做到

  1. 生产环境和测试环境的环境变量的改变
  2. 优化导出速度。文件如果未修改,则使用缓存的文件,而不是强制重新导出
  3. 另外,之前发现导出后光标位置发生改变,会移动到文档第一行首个字符处,使用 save-excursion 函数保存当前 buffer 和当前光标位置,执行完其包含的代码段后恢复 buffer 和光标位置
  4. 使用异步的方式导出笔记。未使用异步。在 org 文档中提到,org-publish-project 函数的最后一个参数改为非空即可创建一个新进程来导出项目。未使用异步是因为在新进程中做不到使用 htmlize 对代码进行高亮

首先,notes/org/ 目录下添加目录 template-test。向其中添加测试环境的内容。
然后,修改 init-func.el 中的笔记导出函数改为

;; 调整 solarized 配色
(defun reload-solarized-termcolors(color)
  (setq solarized-termcolors color)
  (load-theme 'solarized t)
  )

(defun export-my-notes-internal(is-force)
  ;; 配色问题。需要设置为 256 色。否则,在终端下的 emacs 中执行该函数,导出的代码块颜色混乱
  (if(not window-system)
      (reload-solarized-termcolors 256))

  (save-excursion (org-publish-project "notes" is-force nil))  ;; 导出 notes 到 html。is-force 表示是否强制导出

  ;; 导出完毕后,配色再改回来,防止 solarized 在终端中颜色混乱
  (if(not window-system)
      (reload-solarized-termcolors 16))
  )

(defvar *call-export-my-notes-count* 0 "run export-my-notes-internal count")
(defun export-my-notes-test()
  (interactive)
  (if (and (> *call-export-my-notes-count* 0) (equal *site-template-directory* "~/notes/org/templates-test"))
      (progn  ;; 如果连续 2 次导出到测试环境,说明第二次导出时已经时测试环境了,所以就可以使用缓存文件而不必强制重新导出所有文件了
        (export-my-notes-internal nil)
        )
    (progn  ;; 否则,修改环境变量,并强制重新导出所有文件
      (setq *site-template-directory* "~/notes/org/templates-test")
      (setq org-html-head (read-html-template "html-head.html"))
      (setq org-html-preamble (read-html-template "preamble.html"))
      (setq org-html-postamble (read-html-template "postamble.html"))
      (export-my-notes-internal t)
      )
    )
  (incf *call-export-my-notes-count*)  ;; 自增 1
  )

(defun export-my-notes()
  (interactive)
  (if (and (> *call-export-my-notes-count* 0) (equal *site-template-directory* "~/notes/org/templates"))
      (progn  ;; 如果连续 2 次导出到生产环境,说明第二次导出时已经时生产环境了,所以就可以使用缓存文件而不必强制重新导出所有文件了
        (export-my-notes-internal nil)
        )
    (progn  ;; 否则修改环境变量,强制导出
      (setq *site-template-directory* "~/notes/org/templates")
      (setq org-html-head (read-html-template "html-head.html"))
      (setq org-html-preamble (read-html-template "preamble.html"))
      (setq org-html-postamble (read-html-template "postamble.html"))
      (export-my-notes-internal t)
      )
    )
  (incf *call-export-my-notes-count*)
  )

init-keybindings.el 中的快捷键改为

(global-set-key (kbd "C-c e n") 'export-my-notes-test)    ;; 导出 notes 笔记本到 html,测试环境
(global-set-key (kbd "C-c e N") 'export-my-notes)         ;; 生产环境

6 后续优化3 —— 目录聚焦当前大纲

  • 效果一:目录显示当前屏幕位置处的大纲,使用 jquery 来实现
  • 效果二:点击目录显示全部目录,再次点击隐藏

postamble.html 中添加 2 行

<script src="file:/Users/he/notes/html/css/jquery-2.1.3.min.js"></script>
<script src="file:/Users/he/notes/html/css/main.js"></script>

main.js 内容为:

/* 目录显示当前屏幕位置处的大纲 */
window.ego_toc = $('#text-table-of-contents ul li');
if(0 != window.ego_toc.length){
    window.ego_toc_h = $('#table-of-contents h2');
    window.ego_toc_h_text = $('#table-of-contents h2').text();
    window.ego_n = 0;
    window.ego_tmp = ego_n;
    window.ego_head = $(':header').filter('[id*=org]');
    $(window).scroll(function () {
        var startPoint=0;
        var endPoint=ego_head.length-1;
        var offsetValue=window.pageYOffset+60;
        if(ego_head.eq(ego_tmp).offset().top>offsetValue || offsetValue>ego_head.eq((ego_tmp+1)>(ego_head.length-1)?(ego_head.length-1):(ego_tmp+1)).offset().top){
            while((startPoint+1) < endPoint){
                if(ego_head.eq(Math.floor((startPoint+endPoint)/2)).offset().top > offsetValue){
                    endPoint = Math.floor((startPoint+endPoint)/2);
                }
                else if(ego_head.eq(Math.floor((startPoint+endPoint)/2)).offset().top < offsetValue){
                    startPoint = Math.floor((startPoint+endPoint)/2);
                }
                else{
                    break;
                }
            }
            if(offsetValue>ego_head.eq(ego_head.length-1).offset().top){
                ego_n=ego_head.length-1;
            }
            else{
                ego_n = startPoint;
            }

            ego_toc.eq(ego_tmp).children('a').css('color', 'green');
            ego_tmp = ego_n;
            ego_toc.eq(ego_tmp).children('a').css('color', '#3c3c3c');
            if(window.pageYOffset < 10){
                ego_toc_h[0].textContent = ego_toc_h_text;
            }
            else{
                ego_toc_h[0].textContent = ego_toc.eq(ego_tmp)[0].children.item(0).textContent;
            }
            //ego_n = parseInt(ego_str.slice(-1));
        }
    });}

/* 点击目录显示全部目录,再次点击隐藏 */
document.addEventListener('DOMContentLoaded',function() {
    document.getElementById("table-of-contents").onclick = function() {
        var elem = document.getElementById("text-table-of-contents");
        elem.style.display = elem.style.display == "block" ? "none" : "block";
    }
});

7 后续优化4 —— 添加 google 搜索

实现思路:向 google 发送 http get 请求,搜索 www.langdebuqing.com 网站中的内容。
需要在 ~/notes/org/templates/preamble.html 文件中的导航栏的 <ul> 标签中添加:

<li class="search">
    <form action="http://google.com/search" method="get" accept-charset="utf-8">
    <input type="search" id="search" name="q" autocomplete="off" maxlength="30" placeholder="Search..">
    <input type="hidden" name="q" value="site:www.langdebuqing.com">
    </form>
</li>

本笔记系统由 时中贺 搭建和维护,使用 Emacs Org mode 编辑和构建
email: shi_zhonghe@163.com