思源特性提议:外挂样式

背景:为什么需要这个特性

我观察到下面这些现象:

  1. 用户如果需要定制一些细微的样式,需要深入到 theme.css 中修改代码,这对非代码用户极不友好,而这个需求是很广泛的(不然也不需要开一个装修 QQ 群了)

  2. 自定义属性 + 样式,对于主题开发来说已经成为一个特别实用的技巧了,尤其是 Zhang-Light 主题,将其发挥到极致

    image.png

    这种方法可以实现很多很多功能,甚至添加原本没有的新样式。

    实际上,对于用户,可以把添加自定义属性这个过程给封装为更友好的形式(见下文)

  3. 主题作者之间的代码复用问题:一个主题作者有时会开发出适用于任何主题的样式(比如说标题居中、限制页面宽度……),假如其他作者想在自己的主题里引入这个样式,必须修改 theme.css,实际上这个动作是不必要的,万用的样式应该可以独立出主题之外,成为类似于“库”或者“包”一样的存在

于是,我认为外挂样式一定是主题的一个趋势。

下面先介绍对于用户来说,外挂样式这一功能意味着什么。

对于用户

在集市中可以方便地下载和配置外挂样式(参考 VSCode 的插件)

具体能够配置的内容是:样式是否启用,样式的优先级(下文详述)(图里没有,是我懒得画了)

image.png

对于“自定义属性 + 样式”的功能,可以像修改块的外观一样,一键添加自定义属性来用指定的样式显示。

image.png

对于思源和主题开发者:实现细节

由于下面是需要真正落实到代码的一些细节,所以我尽量考虑到现实中会遇到的复杂度。

外挂样式如何工作

外挂样式可以简单理解为是一个 CSS 文件,可以有多个外挂样式,它们可以在主题的 theme.css 之前或之后加载,并且可以覆盖 theme.css,或被 theme.css 覆盖,外挂样式之间也可以相互覆盖。

另外主题也可以将其中的功能提取为外挂样式,这样这些功能也可以在其它主题中应用。

下面举一个例子:(从上到下依次加载 css 文件,删除线代表该样式被后续加载的 CSS 覆盖)

  1. 外挂样式 A:

    • 将自定义属性"f"=="table"的列表显示为表格样式
    • 令上述表格背景为白色
  2. 主题 T:

    • 由于主题为红色调,将标记背景色改为红色
  3. 下载 T 时附带的外挂样式 T.a :

    • 提供一个可开可关的功能:H1 标题居中
  4. 下载 T 时附带的外挂样式 T.b :

    • 提供一个可定制(覆盖)主题 T 的功能:把 T 的主色调改为蓝色
  5. 下载 T 时附带的外挂样式 T.c :

    • 为主题 T 适配外挂样式 A,
    • 由于主题为红色调,将自定义属性"f"=="table"的列表的表格背景改为淡红色 (覆盖外挂样式 A)
  6. 外挂样式 C:

    • 最后加载,优先级最高,未被别的样式覆盖
    • 将标记的背景色强制改为粉红色 (覆盖主题 T)

开发外挂样式的接口参考

外挂样式可以上传到集市,也可以像挂件一样解压使用。

准确来说,外挂样式并非单独的 .css 文件,下载下来的实际是一个外挂样式模块,

下载或解压到 SiYuanWorkspace\conf\appearance\styles 后,文件结构如下

  • SiYuanWorkspace\conf\appearance\styles

    • 某个外挂样式模块的文件夹

      • 若干个 .css 文件

        • 一些 .css 提供了其他 .css 的前置依赖
        • 一些.css 可以在白天主题加载,而另一些则在夜间加载
      • style.json 对外挂样式进行配置

      • 可选的 .js 文件

style.json 文件像下面这样(后面会对每个字段有说明):

{
    "moduleName": "便签",    // 模块名,是会显示给用户的
    "author": "XX",
    "url": "https://github.com/XXX",
    "version": "1.0.0",
    "enable": true,        // 是否启用该样式模块,可以在思源设置界面配置
    "priority": "low",     // 样式模块的优先级,可以在思源设置界面配置

    "cssFiles": [
        {
            "path": "base.css",
            // 该外挂样式模块的依赖css
            // 实际上,可以不在该文件中写出,而直接在主要的.css中import即可
        },
        {
            "path": "stickyNote_DayLight.css",
            "dayOrNight": "dayLight",
            // 日间主题的便签
        },
        {
            "path": "stickyNote_MidNight.css",
            "dayOrNight": "midNight"
            // 夜间主题的便签
        }
    ],
    "click2Add": [
        {
            "buttonLabel": "便签(红)",
            "actionWhenAdding": [
                ["parcelWith", "quote"]
            ],
            "customAttr": [
                {
                    "name": "css",
                    "val": "stickyNote"
                },
                {
                    "name": "stickyNoteCfg",
                    "val": "redBg"
                }
            ]
        },
        {
            "buttonLabel": "便签(蓝)",
            "actionWhenAdding": [
                ["parcelWith", "quote"]
            ],
            "customAttr": [
                {
                    "name": "css",
                    "val": "stickyNote"
                },
                {
                    "name": "stickyNoteCfg",
                    "val": "blueBg"
                }
            ]
        }
    ]
}

其中:

"priority": 优先级(决定了加载顺序)
        "final": 优先级超过主题和其它外挂样式,实现方式是最后才加载(只允许本外挂样式修改其它的外挂样式,不允许其他外挂样式修改本样式。当然这只是一个像!important一样未必总是兑现的承诺)
        "high": 优先级高,一定保证在主题样式之后加载,该样式可以覆盖主题样式,但可能会被其它外挂样式覆盖
        "low" (默认): 优先级低,最先(在主题样式之前)加载,该样式可以被主题和其它外挂样式覆盖

"cssFiles"[] 给出外挂样式模块中的所有css文件,并设置它在白天/夜间主题下启用性,思源主程序将按顺序依次加载对应主题下的css文件
[
{
    "dayOrNight":     白天/夜间主题启用设置
        "both"(默认):    对于日间/夜间主题,均启用css
        "dayLight" || "midNight":     单独为日间/夜间主题启用css

    "loadOnlyIf"[] 如果这个css是为了适配(覆盖)其它外挂样式或主题的,那么可以把对方的模块名或主题名加在这里,
                     只有当思源主程序检测到列表里的样式都加载后,才会加载该css
}
]

"click2Add"[]: 通过点击为块添加特殊的自定义属性,以令该块显示为外挂样式。这个字段为数组,给出可以由用户点击添加的样式自定义属性
[
    {
        "buttonLabel": 显示给用户的,样式按钮标签
        "relatedBlock": ["p", ...] : 可以给那些种类的块添加这种样式。缺省时为对所有块均启用
        "customAttr"[]: 想要添加的自定义属性列表
        [
            {
                "name": 自定义属性名
                "val": 自定义属性值
            }
        ]
        "actionWhenAdding"[]:     添加自定义属性的同时,会执行的特殊行为的列表
                                  假如该列表为空,则默认行为是:给每个选中的块添加“customAttr”中指定的属性
                                  假如该列表不为空,则默认行为被禁用,然后该列表中的各个特殊行为按下标排序从小到大执行

            其中的每个特殊行为均以数组给出,第一个元素为操作名,后面的元素为参数:
            ["replace", regExp , newExp ] :     对选中的块的 Kramdown(或Json)源码进行一次正则替换
            ["parcelWith", "quote"]:     为一个非引述块或多个块添加外挂样式时,先将这些块用一个引述块包裹,然后对包裹的引述块添加自定义属性,并结束特殊行为
            ["parcelWith", "super"]:     同上,只不过改为用超级块包裹
            // 感觉对源码的若干次正则替换已经可以解决绝大多数情景了,后两者也可以用正则替换实现,不过单独列出来方便主题开发者开发
    }
]

可以看出,虽然一个外部样式模块可能包含多个.css 文件,但是在日间/夜间模式确定的情况下,大多数模块里主要的.css 文件只有一个,因此多数时候可以把外部样式模块简单视为一个.css 文件。

主题和外挂样式的交互

主题的 theme.json 配置文件中可以添加一个字段 "externStyles"

{
    "name": "daylight",
    "author": "Vanessa",
    "url": "https://github.com/Vanessa219",
    "version": "1.0.0",
    "modes": [
      "light"
    ],
    "externStyles":[
        {
            "moduleName": "foo",    // 和对应的外挂样式模块的命名一致,不需要在这里指定路径,由思源主程序负责寻找本地路径加载或联网下载
            "url": "https://github.com/...",
            "isEnablingSuggested": true    // 主题建议,该外挂样式模块是否推荐启用
                                           // 如果建议启用,则从集市下载主题的时候同时也下载该外挂样式,并且在每次启用主题的时候顺带会启用该外挂样式
                                           // 如果不建议启用,则从集市下载主题的时候不会同时下载该外挂样式,并且在每次启用主题的时候顺带会禁用这个外挂样式
        }
    ]
}

下载主题时“附带”的外挂样式并不隶属于主题,仍然是独立的外挂样式,可以单独设置外挂样式的启用与否。之所以写在主题配置文件里,单纯是因为主题作者认为这个样式符合主题风格或者很漂亮。(就像你下载了思源,里面用到了 Pandoc,并不意味着 Pandoc 隶属于思源)

假如想对其它外挂样式进行适配,应该引用一个专门用于定制其它外挂样式的自定义外挂样式模块,这个样式模块应该像下面这样:

{
    "moduleName": "XX主题:定制外部样式", // 样式模块名,是会显示给用户的
    "author": "XX",
    "url": "https://github.com/XXX",
    "version": "1.0.0",
    "enable": true, // 是否启用该样式模块,可以在思源设置界面配置
    "priority": "high" || "final", // 为了覆盖别的样式模块,应该提高优先级
    "cssFiles": [
        {
            "path": "stickyNote_ForXXX.css",
            "loadOnlyIf"[
                "XXX(本主题)", "便签"     // 需要本主题和对应模块均加载时才进行定制
            ]
        },
        {
            "path": "list2Table_ForXXX.css",
            "loadOnlyIf"[
                "XXX(本主题)", "列表显示为表格"
            ]
        }
    ]
}

需要特别注意的是:主题附带的外挂样式对于主题来说均是可选的,用户可以选择把这些外挂样式全部关闭。假如主题依赖于某个 css,那不应该把它分离成外挂样式,这也不是外挂样式这个概念出现想要解决的问题。外挂样式是面向用户而非开发者的,不应该完全等同于编程中的“库”,它更像思源的挂件,每个外挂样式应该提供一个完整的样式功能。

另外也不要在一个样式模块里面添加多种样式,一个模块对应一个功能即可,方便开启关闭。
(就像卡片式笔记一样,追求独立化)
甚至可以为“H1 标题居中”、"H2 居中"、"H3 居中"……分别写一个模块。(主要是为了方便非码农用户定制,能不进.css 改代码就是最大的成功)

对主题大佬的一些希望

  • 希望一些主题大佬率先把一些常见的功能作为外挂样式分离出来,作为公用的样式

    • (例子太多了,比如便签、语雀提示区块、额外的色彩、额外的字体、像挖空、波浪线、备注等这类特殊的样式)
    • 这一方面能节省其它主题开发者的时间,一方面也尽早把与样式相关的自定义块属性统一下来
  • 编写外挂样式的时候尽量使用 :root{...} 中的配色

  • 编写外挂样式的时候,优先级尽量低,方便别人定制

结语

假如可以调用 JS 脚本的话,那肯定会方便不少,比如"loadOnlyIf"接口就可以简化了。不过我不太清楚对于思源,JS 的界限在哪里,所以上文都是假设不使用样式定制的 JS。不过这么设计也行,可以减少开发样式的代码量,毕竟大多数代码由思源开发者搞定了。

对于用户来说,很友好,不用再到 theme.css 里面修改什么了,也不用手动添加自定义块属性了。

而且自定义块属性配合样式,确实可以实现很多功能,像是标题居中之类的功能,其实没必要用插件实现,用样式刚刚好。

而且对于主题开发者来说,模块化和结构化肯定是必然趋势,毕竟不是每个人都要造轮子的。

上面给出的接口设计,由于是空想的,仅供参考,欢迎大家讨论。

2 操作
EndlessErrors 在 2022-04-13 11:08:30 更新了该帖
EndlessErrors 在 2022-04-13 11:00:01 更新了该帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • szukkboy
    订阅者

    非常认真的题主~~~

  • 8V9q7V
    支持者 订阅者

    +1

  • bug320
    订阅者

    +1,非常期待

  • 88250
    订阅者

    感谢提供思路,目前已经支持 theme.js,理论上主题可以自我完成外部样式加载。

    暂时不考虑提供统一机制,因为现在还缺乏模块化规范,这个规范由我们制定可能不太合适,由社区实践进行归纳比较合适。我们还是把主要精力放在开发主线计划上,再次感谢提议。

    1 回复
  • foolishman83

    看得出来题主花费了很大心思,支持 +1。对于样式来说,一个主题通常是符合个人的样式审美和功能需要,但是样式和功能往往是因人而异的,比如集市上的几个主题,每个大佬都有自己的创新之处,个人喜欢的样式和功能通常散落在各处,这样对非技术用户不是很友好,希望把一些创新的样式功能单独模块化,可以自由组合,选择加载。这当然给各位主题大佬增加了很大的工作量,但对社区来说,或许是是更可持续的。

  • EndlessErrors
    支持者 订阅者 作者

    其实这个概念主要是面向用户,给非码农用户在自定义主题的细节时提供方便的。

  • EndlessErrors
    支持者 订阅者 作者

    经 D 大提醒,我想了一个现阶段的替代方案,就是在主题的 theme.js 里面扫描 XXtheme/externStyles 下的所有 css 文件并逐一添加,可以暂时实现动态添加 CSS 的功能。如果需要定制一些功能,直接删除或者添加该目录下的 css 文件即可,不需要进 theme.css 里修改什么代码。

请输入回帖内容 ...