Bump to 1.0.4.
1. App : Add HelpView. 2. SerialPortAssistant: Add input validation.
This commit is contained in:
parent
dde53267ea
commit
0a340aff4b
|
@ -35,7 +35,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
version: ${{ matrix.qt_ver }}
|
version: ${{ matrix.qt_ver }}
|
||||||
arch: ${{ matrix.qt_arch }}
|
arch: ${{ matrix.qt_arch }}
|
||||||
modules: 'qt5compat qtmultimedia qtshadertools qtimageformats qtserialport'
|
modules: 'qt5compat qtmultimedia qtshadertools qtimageformats qtserialport qtwebview'
|
||||||
|
|
||||||
- name: Set up Ninja
|
- name: Set up Ninja
|
||||||
uses: seanmiddleditch/gha-setup-ninja@v3
|
uses: seanmiddleditch/gha-setup-ninja@v3
|
||||||
|
|
|
@ -35,7 +35,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
version: ${{ matrix.qt_ver }}
|
version: ${{ matrix.qt_ver }}
|
||||||
arch: ${{ matrix.qt_arch }}
|
arch: ${{ matrix.qt_arch }}
|
||||||
modules: 'qt5compat qtmultimedia qtshadertools qtimageformats qtserialport'
|
modules: 'qt5compat qtmultimedia qtshadertools qtimageformats qtserialport qtwebview'
|
||||||
|
|
||||||
- name: Set up Ninja
|
- name: Set up Ninja
|
||||||
uses: seanmiddleditch/gha-setup-ninja@v3
|
uses: seanmiddleditch/gha-setup-ninja@v3
|
||||||
|
|
|
@ -36,7 +36,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
version: ${{ matrix.qt_ver }}
|
version: ${{ matrix.qt_ver }}
|
||||||
arch: ${{ matrix.qt_arch }}
|
arch: ${{ matrix.qt_arch }}
|
||||||
modules: 'qt5compat qtmultimedia qtshadertools qtimageformats qtserialport'
|
modules: 'qt5compat qtmultimedia qtshadertools qtimageformats qtserialport qtwebview'
|
||||||
|
|
||||||
- name: Set up Ninja
|
- name: Set up Ninja
|
||||||
uses: seanmiddleditch/gha-setup-ninja@v3
|
uses: seanmiddleditch/gha-setup-ninja@v3
|
||||||
|
|
|
@ -36,7 +36,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
version: ${{ matrix.qt_ver }}
|
version: ${{ matrix.qt_ver }}
|
||||||
arch: ${{ matrix.qt_arch }}
|
arch: ${{ matrix.qt_arch }}
|
||||||
modules: 'qt5compat qtmultimedia qtshadertools qtimageformats qtserialport'
|
modules: 'qt5compat qtmultimedia qtshadertools qtimageformats qtserialport qtwebview'
|
||||||
|
|
||||||
- name: Set up Ninja
|
- name: Set up Ninja
|
||||||
uses: seanmiddleditch/gha-setup-ninja@v3
|
uses: seanmiddleditch/gha-setup-ninja@v3
|
||||||
|
|
|
@ -43,7 +43,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
version: ${{ matrix.qt_ver }}
|
version: ${{ matrix.qt_ver }}
|
||||||
arch: ${{ matrix.qt_arch }}
|
arch: ${{ matrix.qt_arch }}
|
||||||
modules: 'qt5compat qtmultimedia qtshadertools qtimageformats qtserialport'
|
modules: 'qt5compat qtmultimedia qtshadertools qtimageformats qtserialport qtwebview'
|
||||||
|
|
||||||
- name: Qt6 environment configuration
|
- name: Qt6 environment configuration
|
||||||
if: ${{ startsWith( matrix.qt_ver, 6 ) }}
|
if: ${{ startsWith( matrix.qt_ver, 6 ) }}
|
||||||
|
|
|
@ -43,7 +43,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
version: ${{ matrix.qt_ver }}
|
version: ${{ matrix.qt_ver }}
|
||||||
arch: ${{ matrix.qt_arch }}
|
arch: ${{ matrix.qt_arch }}
|
||||||
modules: 'qt5compat qtmultimedia qtshadertools qtimageformats qtserialport'
|
modules: 'qt5compat qtmultimedia qtshadertools qtimageformats qtserialport qtwebview'
|
||||||
|
|
||||||
- name: Qt6 environment configuration
|
- name: Qt6 environment configuration
|
||||||
if: ${{ startsWith( matrix.qt_ver, 6 ) }}
|
if: ${{ startsWith( matrix.qt_ver, 6 ) }}
|
||||||
|
|
|
@ -46,7 +46,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
version: ${{ matrix.qt_ver }}
|
version: ${{ matrix.qt_ver }}
|
||||||
arch: ${{ matrix.qt_arch }}
|
arch: ${{ matrix.qt_arch }}
|
||||||
modules: 'qt5compat qtmultimedia qtshadertools qtimageformats qtserialport'
|
modules: 'qt5compat qtmultimedia qtshadertools qtimageformats qtserialport qtwebview'
|
||||||
|
|
||||||
- name: msvc-build
|
- name: msvc-build
|
||||||
id: build
|
id: build
|
||||||
|
|
|
@ -46,7 +46,7 @@ jobs:
|
||||||
with:
|
with:
|
||||||
version: ${{ matrix.qt_ver }}
|
version: ${{ matrix.qt_ver }}
|
||||||
arch: ${{ matrix.qt_arch }}
|
arch: ${{ matrix.qt_arch }}
|
||||||
modules: 'qt5compat qtmultimedia qtshadertools qtimageformats qtserialport'
|
modules: 'qt5compat qtmultimedia qtshadertools qtimageformats qtserialport qtwebview'
|
||||||
|
|
||||||
- name: msvc-build
|
- name: msvc-build
|
||||||
id: build
|
id: build
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
Subproject commit 18edf9f0fdf0f11663d328a8166486226246e656
|
Subproject commit c8d7d6d4689b422bdbe7cf1ef0cdcaf2948aeee6
|
|
@ -1,6 +1,6 @@
|
||||||
cmake_minimum_required(VERSION 3.20)
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
|
||||||
project(ProtocolParser_APP VERSION 1.0.3 LANGUAGES CXX)
|
project(ProtocolParser_APP VERSION 1.0.4 LANGUAGES CXX)
|
||||||
|
|
||||||
set(RIBBONUI_BUILD_FRAMELESSHEPLER ON)
|
set(RIBBONUI_BUILD_FRAMELESSHEPLER ON)
|
||||||
set(RIBBONUI_BUILD_EXAMPLES OFF)
|
set(RIBBONUI_BUILD_EXAMPLES OFF)
|
||||||
|
|
291
README.md
291
README.md
|
@ -2,10 +2,10 @@
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="app_source/resources/imgs/icon.png" alt="Logo" style="width:40%; height:auto;">
|
<img src="app_source/resources/imgs/icon.png" alt="Logo" style="width:40%; height:auto;">
|
||||||
</div>
|
</div>
|
||||||
<h1 align="center"> ProtocolParser 协议解析器 V1.0.3</h1>
|
<h1 align="center"> ProtocolParser 协议解析器 V1.0.4</h1>
|
||||||
|
|
||||||
## 一、介绍
|
## 一、介绍
|
||||||
这是一个用于与DKY的THM3682实验箱搭配使用的上位机协议解析器,具体协议如「通讯协议解析」所示,后续功能使用说明待补充。
|
这是一个用于与DKY的THM3682实验箱搭配使用的上位机协议解析器,具体如何使用请看[使用说明](#二使用说明)。
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<div align="center">
|
<div align="center">
|
||||||
<img src="documents/pictures/home-light.png" alt="主界面(浅色)" style="width:45%; height:auto;">
|
<img src="documents/pictures/home-light.png" alt="主界面(浅色)" style="width:45%; height:auto;">
|
||||||
|
@ -14,271 +14,22 @@
|
||||||
<p align="center"> 主界面(浅色/深色模式) </p>
|
<p align="center"> 主界面(浅色/深色模式) </p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
## 二、通讯协议解析
|
## 二、使用说明
|
||||||
|
## 1. [协议解析](documents/protocol.md)
|
||||||
- [通讯协议解析](#通讯协议解析)
|
+ 概述
|
||||||
- [1. 概述](#1-概述)
|
+ 密钥
|
||||||
- [2. 密钥](#2-密钥)
|
+ 认证流程
|
||||||
- [3. 认证流程](#3-认证流程)
|
+ 数据结构
|
||||||
- [3.1 非对称双向认证](#31-非对称双向认证)
|
+ 接口解析
|
||||||
- [3.2 对称双向认证](#32-对称双向认证)
|
## 2. [串口使用](documents/serialport.md)
|
||||||
- [4. 数据结构](#4-数据结构)
|
+ 参数配置
|
||||||
- [4.1 基础帧(base\_frame)](#41-基础帧base_frame)
|
+ 串口消息设置
|
||||||
- [4.2 数据帧(data\_frame)](#42-数据帧data_frame)
|
+ 串口助手设置
|
||||||
- [4.3 数字信封(digi\_env)](#43-数字信封digi_env)
|
+ 串口数据流向
|
||||||
- [4.4 非对称双向认证包(ssl\_frame)](#44-非对称双向认证包ssl_frame)
|
## 3. [紫蜂协议](documents/zigbee.md)
|
||||||
- [4.5 对称双向认证包(hmac\_frame)](#45-对称双向认证包hmac_frame)
|
+ 设备列表
|
||||||
- [4.6 对称加密数据帧(crypto\_zdata\_frame)](#46-对称加密数据帧crypto_zdata_frame)
|
+ 密钥管理
|
||||||
- [4.7 设备信息存储(device)](#47-设备信息存储device)
|
+ 调试选项
|
||||||
- [5. 接口解析](#5-接口解析)
|
## 4. [其他](documents/others.md)
|
||||||
- [5.1 协议封装/解析](#51-协议封装解析)
|
+ 主题
|
||||||
- [5.1.1 void protocal\_wrapper(data\_frame \*frame, u8 type, u16 length, u8 \*data, bool use\_crc)](#511-void-protocal_wrapperdata_frame-frame-u8-type-u16-length-u8-data-bool-use_crc)
|
+ 数据结构自定义
|
||||||
- [5.1.2 void base\_frame\_maker(void \*in\_frame, base\_frame \*out\_frame, u16 dest\_addr,device \*dev,u16 node\_addr=0)](#512-void-base_frame_makervoid-in_frame-base_frame-out_frame-u16-dest_addrdevice-devu16-node_addr0)
|
|
||||||
- [5.1.3 bool base\_frame\_parser(base\_frame \*in\_frame, void \*\*out\_frame, device \*dev)](#513-bool-base_frame_parserbase_frame-in_frame-void-out_frame-device-dev)
|
|
||||||
- [5.1.4 void ssl\_frame\_maker(ssl\_frame \*frame, u8 \*data, int data\_len)](#514-void-ssl_frame_makerssl_frame-frame-u8-data-int-data_len)
|
|
||||||
- [5.1.5 void zigbee\_data\_encrypt(data\_frame \*data, crypto\_zdata\_frame *zdata, bool (* SM4\_encrypt)(u8 \*key\_origin, u32 key\_len, u8 \*in\_origin, u32 in\_len, u8 \*out, u32 \*out\_len, bool use\_real\_cbc),QString en\_key = "")](#515-void-zigbee_data_encryptdata_frame-data-crypto_zdata_frame-zdata-bool--sm4_encryptu8-key_origin-u32-key_len-u8-in_origin-u32-in_len-u8-out-u32-out_len-bool-use_real_cbcqstring-en_key--)
|
|
||||||
- [5.1.6 bool zigbee\_data\_dectypt(data\_frame \*data, crypto\_zdata\_frame *zdata,bool (* SM4\_decrypt)(u8 \*key\_origin, u32 key\_len, u8 \*in, u32 in\_len, u8 \*out, u32 \*out\_len, bool use\_real\_cbc),QString en\_key = "")](#516-bool-zigbee_data_dectyptdata_frame-data-crypto_zdata_frame-zdatabool--sm4_decryptu8-key_origin-u32-key_len-u8-in-u32-in_len-u8-out-u32-out_len-bool-use_real_cbcqstring-en_key--)
|
|
||||||
- [5.2 协议认证/验证](#52-协议认证验证)
|
|
||||||
- [5.2.1 void HMAC\_identify(device \*self, device \*node, hmac\_frame \*hframe, void (\*sendTonode)(ZigbeeFrame \&data), void (\*SM3\_HMAC)(u8 \*key, int keylen,u8 \*input, int ilen,u8 output\[32\]))](#521-void-hmac_identifydevice-self-device-node-hmac_frame-hframe-void-sendtonodezigbeeframe-data-void-sm3_hmacu8-key-int-keylenu8-input-int-ilenu8-output32)
|
|
||||||
- [5.2.2 bool data\_frame\_verify(data\_frame \*frame)](#522-bool-data_frame_verifydata_frame-frame)
|
|
||||||
- [5.2.3 void HMAC\_changeVerifykey(u8 key\[16\], device\* self, device \*node, void (*sendTonode)(ZigbeeFrame \&data),bool (* SM4\_encrypt)(u8 \*key\_origin, u32 key\_len, u8 \*in\_origin, u32 in\_len, u8 \*out, u32 \*out\_len, bool use\_real\_cbc))](#523-void-hmac_changeverifykeyu8-key16-device-self-device-node-void-sendtonodezigbeeframe-databool--sm4_encryptu8-key_origin-u32-key_len-u8-in_origin-u32-in_len-u8-out-u32-out_len-bool-use_real_cbc)
|
|
||||||
- [5.3 工具](#53-工具)
|
|
||||||
- [5.3.1 uint16\_t crc16\_xmodem(const uint8\_t \*buffer, uint32\_t buffer\_length)](#531-uint16_t-crc16_xmodemconst-uint8_t-buffer-uint32_t-buffer_length)
|
|
||||||
- [5.3.2 bool bytecmp(u8 \*a, u8 \*b, u16 length)](#532-bool-bytecmpu8-a-u8-b-u16-length)
|
|
||||||
|
|
||||||
## 1. 概述
|
|
||||||
本协议是一套运行在应用层的,用于端到端通讯的轻量级安全协议,支持非对称/对称的双向、单向认证及加密通讯。该协议的认证和加密通讯由SM系列(SM2、SM3、SM4)算法保障,安全性较强,可抵御重放攻击。协议易拓展,数据包采用二进制传输,适合在多种窄带宽场景下使用(目前已在嵌入式、ARM、X86/64平台上测试通过)。
|
|
||||||
|
|
||||||
## 2. 密钥
|
|
||||||
本协议采用两类密钥:一是用于双向认证的SM2公私钥对,二是用于对称双、单向认证和加密传输的SM4密钥。
|
|
||||||
+ SM2密钥长度为:公钥64Bytes(512Bits),私钥32 Bytes(256Bits)。密文长度为明文长度再加上96Bytes。签名长度为64Bytes(被签名数据内包含随机数盐值,因此对同一数据签名的结果不相同)。
|
|
||||||
+ SM4密钥长度为:16Bytes(128Bits)。
|
|
||||||
|
|
||||||
## 3. 认证流程
|
|
||||||
### 3.1 非对称双向认证
|
|
||||||
由于SM2算法会占用一部分资源,非对称双向认证一般用于性能较好的终端(如性能较强的单片机、树莓派或者PC等设备)之间的认证,性能较差的单片机将会出现死机等预料外情况(板载硬件密码算法模块除外)。
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant 服务器
|
|
||||||
participant 客户端
|
|
||||||
客户端-->服务器: 客户端验证服务器
|
|
||||||
客户端->>服务器: 包含‘Hello’信息的ssl_frame请求
|
|
||||||
服务器->>客户端: 包含‘Hello’和64Bytes服务器公钥信息的ssl_frame响应
|
|
||||||
客户端-->>客户端: 用服务器公钥加密自己的公钥
|
|
||||||
客户端->>服务器: 包含服务器公钥加密的客户端公钥信息的ssl_frame数据包
|
|
||||||
服务器-->>服务器: 用自己的私钥解密出客户端公钥
|
|
||||||
服务器->>客户端: 包含用客户端公钥加密的‘Verified’信息的ssl_frame数据包
|
|
||||||
客户端-->>客户端: 用自己的私钥解密出‘Verified’信息
|
|
||||||
客户端->>服务器: 包含服务器公钥加密的‘OK’信息的ssl_frame数据包
|
|
||||||
服务器-->客户端: 服务器验证客户端
|
|
||||||
服务器-->>服务器: 生成8Bytes随机数挑战值
|
|
||||||
服务器->>客户端: 包含客户端公钥加密的8Bytes挑战值信息的ssl_frame数据包
|
|
||||||
客户端-->>客户端: 用自己的私钥解密出挑战值,<br>并用自己的私钥对它签名
|
|
||||||
客户端->>服务器: 包含客户端公钥(64Bytes)、签名(64Bytes)、<br>挑战值(8Bytes)共136Bytes数据的ssl_frame数据包
|
|
||||||
服务器-->>服务器: 进行三轮比对:<br>1.将获得的客户端公钥与<br>第一轮验证中获取的客户端公钥进行比对<br>2.将获得的挑战值与发送的进行比对<br>3.用客户端公钥对挑战值签名数据进行验签
|
|
||||||
服务器->>客户端: 包含客户端公钥加密的‘OK’信息的ssl_frame数据包
|
|
||||||
服务器-->客户端: 双向认证结束
|
|
||||||
```
|
|
||||||
### 3.2 对称双向认证
|
|
||||||
对称双向认证使用基于SM3的HMAC算法实现。通过验证双方是否共同持有同一对预设定密钥来实现认证,在实际运用过程中,为节省资源,服务器发送给客户端的‘Identified’信息可不加密,采用明文传输(因为后续传输的数据包均为加密数据包)。
|
|
||||||
```mermaid
|
|
||||||
sequenceDiagram
|
|
||||||
participant 服务器
|
|
||||||
participant 客户端
|
|
||||||
客户端-->>客户端: 生成1Byte随机数,并对其使用基于SM3的HMAC算法和<br>预设定16Bytes密钥生成值
|
|
||||||
客户端->>服务器: 包含随机数和使用预设定16Bytes密钥生成的<br>HMAC值信息的hmac_frame请求
|
|
||||||
服务器-->>服务器: 使用自己拥有的预设定16Bytes密钥对收到的<br>1Byte随机数进行HMAC运算,并将值与收到值进行比对
|
|
||||||
服务器->>客户端: 包含使用预设定16Bytes密钥加密的<br>‘Identified’信息的crypto_zdata_frame数据包
|
|
||||||
客户端-->>客户端: 用预设定16Bytes密钥解密出‘Identified’信息
|
|
||||||
客户端->>服务器: 包含正常数据的用预设定密钥加密的<br>crypto_zdata_frame数据包
|
|
||||||
```
|
|
||||||
|
|
||||||
## 4. 数据结构
|
|
||||||
### 4.1 基础帧(base_frame)
|
|
||||||
基础帧是本协议传输最底层的帧,用于承载其他帧。
|
|
||||||
| 帧结构 | 长度(Bytes) | 说明 |
|
|
||||||
|:----:|:----:|:----:|
|
|
||||||
| head | 2 | 包头,数值固定为`0xAAAD` |
|
|
||||||
| ori_addr | 2 | 源地址,即该帧发送方的地址 |
|
|
||||||
| des_addr | 2 | 目的地址,即该帧接收方的地址 |
|
|
||||||
| node_addr | 2 | 节点地址,仅在此帧被服务器进行路由时使用,<br>用以标记该帧由哪个节点最先产生 |
|
|
||||||
| id | 2 | 帧标号,用以与`rand_num`共同唯一标记一个帧,防止重放攻击 |
|
|
||||||
| length | 2 | 帧长度(包括帧头),最大为65535个Bits |
|
|
||||||
| reset_num | 1 | 重置标记位,用以告知对方在接收此帧后,<br>下一帧的帧标号将归零,`rand_num`将被重新生成 |
|
|
||||||
| rand_num | 1 | 随机数 |
|
|
||||||
| data | 可变长度 | 数据最大不超过(8191 - `BASE_FRAME_PREFIX_LEN`)= 8177 |
|
|
||||||
|
|
||||||
另有其他说明如下:
|
|
||||||
+ 宏定义`BASE_FRAME_PREFIX_LEN`:基础帧帧头长度,固定为14Bytes
|
|
||||||
+ 宏定义`BASE_FRAME_HEAD`:基础帧帧头,固定为`0xAAAD`
|
|
||||||
+ 宏定义`BASE_FRAME_RESET_NUM`:基础帧重置阈值:当发送帧数达到该阈值时,下一帧将被重置帧标号和随机数,该阈值一般为`10000`,最大不超过`65535`
|
|
||||||
+ 宏定义`new_base_frame(num)`:用以生成基础帧数据结构,`num`为`data`部分长度,使用时请注意强制类型转换
|
|
||||||
### 4.2 数据帧(data_frame)
|
|
||||||
数据帧是本协议传输中最顶层的帧,用于承载各类数据。
|
|
||||||
| 帧结构 | 长度(Bytes) | 说明 |
|
|
||||||
|:----:|:----:|:----:|
|
|
||||||
| head | 2 | 包头,数值固定为`0xAAAA` |
|
|
||||||
| type | 1 | 数据类型,由用户根据业务不同自定义 |
|
|
||||||
| use_crc | 1 | CRC标记位,当其为`0xFF`时将对数据进行CRC校验 |
|
|
||||||
| data_length | 2 | 数据长度,不包括包头 |
|
|
||||||
| crc | 2 | CRC16校验码,当CRC标记位使能且对数据进行CRC校验后数值与该码不同,包将被判定为损坏 |
|
|
||||||
| data | 可变长度 | 根据底层帧的不同,数据最大不超过(8191 - `BASE_FRAME_PREFIX_LEN` - `DATA_FRAME_PREFIX_LEN`)= 8169,若叠加多重帧,需要减去对应帧的帧头 |
|
|
||||||
|
|
||||||
另有其他说明如下:
|
|
||||||
+ 宏定义`DATA_FRAME_PREFIX_LEN`:数据帧帧头长度,固定为8Bytes
|
|
||||||
+ 宏定义`DATA_FRAME_HEAD`:数据帧帧头,固定为`0xAAAA`
|
|
||||||
+ 宏定义`new_data_frame(num)`:用以生成数据帧数据结构,`num`为`data`部分长度,使用时请注意强制类型转换
|
|
||||||
### 4.3 数字信封(digi_env)
|
|
||||||
数字信封是通信双方进行非对称双向认证后,用于加密传输数据的数据结构。
|
|
||||||
| 帧结构 | 长度(Bytes) | 说明 |
|
|
||||||
|:----:|:----:|:----:|
|
|
||||||
| head | 2 | 包头,数值固定为`0xAAAB` |
|
|
||||||
| length | 2 | 长度,包括包头 |
|
|
||||||
| crypted_session_key | 112 | 由接收方公钥加密的16Bytes会话密钥再加上96Bytes的额外数据 |
|
|
||||||
| data | 可变长度 | 由会话密钥进行SM4加密的数据长度,数据最大不超过(8191 - `BASE_FRAME_PREFIX_LEN` - `DIGI_ENV_PREFIX_LEN` - `DIGI_ENV_SESSION_KEY_LEN`)= 8061,又因SM4的密文为16的倍数,因此最大不能超过8048 |
|
|
||||||
|
|
||||||
另有其他说明如下:
|
|
||||||
+ 宏定义`SM4_PADDING_LEN`:SM4默认填充长度,固定为16Bytes
|
|
||||||
+ 宏定义`DIGI_ENV_PREFIX_LEN`:数字信封包头长度,固定为4Bytes
|
|
||||||
+ 宏定义`DIGI_ENV_SESSION_KEY_LEN`:数字信封密态会话密钥长度,固定为112Bytes
|
|
||||||
+ 宏定义`DIGI_ENV_HEAD`:数字信封包头,固定为`0xAAAB`
|
|
||||||
+ 宏定义`new_digi_env(num)`:用以生成数字信封数据结构,`num`为`data`部分长度,使用时请注意强制类型转换
|
|
||||||
### 4.4 非对称双向认证包(ssl_frame)
|
|
||||||
非对称双向认证包负责承载非对称双向认证相关数据。
|
|
||||||
| 帧结构 | 长度(Bytes) | 说明 |
|
|
||||||
|:----:|:----:|:----:|
|
|
||||||
| head | 2 | 包头,数值固定为`0xAAAC` |
|
|
||||||
| length | 2 | 长度,包括包头 |
|
|
||||||
| data | 可变长度 | 数据最大不超过(8191 - `BASE_FRAME_PREFIX_LEN` - `SSL_FRAME_PREFIX_LEN`)= 8173 |
|
|
||||||
|
|
||||||
另有其他说明如下:
|
|
||||||
+ 宏定义`SSL_FRAME_PREFIX_LEN`:非对称双向认证包包头长度,固定为4Bytes
|
|
||||||
+ 宏定义`SSL_FRAME_HEAD`:非对称双向认证包包头,固定为`0xAAAC`
|
|
||||||
+ 宏定义`new_ssl_frame(num)`:用以生成非对称双向认证包数据结构,`num`为`data`部分长度,使用时请注意强制类型转换
|
|
||||||
### 4.5 对称双向认证包(hmac_frame)
|
|
||||||
对称双向认证包负责承载对称双向认证相关数据。
|
|
||||||
| 帧结构 | 长度(Bytes) | 说明 |
|
|
||||||
|:----:|:----:|:----:|
|
|
||||||
| head | 2 | 包头,数值固定为`0xAAAE` |
|
|
||||||
| length | 2 | 长度,包括包头 |
|
|
||||||
| value | 1 | 随机数 |
|
|
||||||
| hmac | 32 | 随机数`value`对应的HMAC数值 |
|
|
||||||
|
|
||||||
另有其他说明如下:
|
|
||||||
+ 宏定义`HMAC_FRAME_PREFIX_LEN`:对称双向认证包包头长度,固定为4Bytes
|
|
||||||
+ 宏定义`HMAC_FRAME_HEAD`:非对称双向认证包包头,固定为`0xAAAE`
|
|
||||||
### 4.6 对称加密数据帧(crypto_zdata_frame)
|
|
||||||
对称加密数据帧用于通信双方完成对称双向认证后数据加密传输。
|
|
||||||
| 帧结构 | 长度(Bytes) | 说明 |
|
|
||||||
|:----:|:----:|:----:|
|
|
||||||
| head | 2 | 包头,数值固定为`0xAAAF` |
|
|
||||||
| length | 2 | 长度,包括包头 |
|
|
||||||
| crc | 2 | CRC16校验码,对解密数据进行CRC校验后数值与该码不同,将被判定为解密失败 |
|
|
||||||
| data | 可变长度 | 数据最大不超过(8191 - `BASE_FRAME_PREFIX_LEN` - `CRYPTO_ZDATA_FRAME_PREFIX_LEN`)= 8171 |
|
|
||||||
|
|
||||||
另有其他说明如下:
|
|
||||||
+ 宏定义`CRYPTO_ZDATA_FRAME_PREFIX_LEN`:对称加密数据帧帧头长度,固定为6Bytes
|
|
||||||
+ 宏定义`CRYPTO_ZDATA_FRAME_HEAD`:对称加密数据帧帧头,固定为`0xAAAF`
|
|
||||||
### 4.7 设备信息存储(device)
|
|
||||||
协议采用`device`结构体用作存储相关通讯数据。
|
|
||||||
| 结构 | 说明 |
|
|
||||||
|:----:|:----:|
|
|
||||||
| addr | 用以保存通讯对象的地址(可以是发送方,也可以是接收方) |
|
|
||||||
| id | 缓存发送/接收到的基础帧帧标号 |
|
|
||||||
| rand_num | 缓存发送/接收到的基础帧随机数 |
|
|
||||||
| verified | 标记该通讯对象是否通过认证 |
|
|
||||||
| online | 标记该通讯对象是否在线 |
|
|
||||||
| logined | 仅作为服务器时使用,记录该通讯对象是否已经登录 |
|
|
||||||
| stage | 仅在非对称双向认证时使用,记录该通讯对象正处于认证的第几阶段 |
|
|
||||||
| chlg_buf | 仅在非对称双向认证时使用,记录该通讯对象的挑战值 |
|
|
||||||
| ip | 仅在TCP链路中使用,记录该通讯对象的IP地址 |
|
|
||||||
| port | 仅在TCP链路中使用,记录该通讯对象的端口号 |
|
|
||||||
| key_pair | 仅在非对称双向认证时使用,记录该通讯对象的公钥和会话密钥 |
|
|
||||||
|
|
||||||
|
|
||||||
## 5. 接口解析
|
|
||||||
### 5.1 协议封装/解析
|
|
||||||
#### 5.1.1 void protocal_wrapper(data_frame *frame, u8 type, u16 length, u8 *data, bool use_crc)
|
|
||||||
将对应数据打包进数据帧中。
|
|
||||||
| 变量名 | 输入/输出 | 说明 |
|
|
||||||
|:----:|:----:|:----:|
|
|
||||||
| frame | 输出 | 数据帧指针 |
|
|
||||||
| type | 输入 | 数据类型 |
|
|
||||||
| length | 输入 | 数据长度 |
|
|
||||||
| use_crc | 输入 | 是否启用CRC验证 |
|
|
||||||
#### 5.1.2 void base_frame_maker(void *in_frame, base_frame *out_frame, u16 dest_addr,device *dev,u16 node_addr=0)
|
|
||||||
将对应数据打包进基础帧中。
|
|
||||||
| 变量名 | 输入/输出 | 说明 |
|
|
||||||
|:----:|:----:|:----:|
|
|
||||||
| in_frame | 输入 | 数据包指针,指向需要被打包进基础帧的数据地址 |
|
|
||||||
| out_frame | 输出 | 基础帧指针 |
|
|
||||||
| dest_addr | 输入 | 目的地址 |
|
|
||||||
| dev | 输出 | 发送方的`device`数据结构指针,缓存帧标号和随机数等相关数据 |
|
|
||||||
| node_addr | 输入 | 仅在服务器端路由时使用,用以保存该基础帧的最初创建者地址 |
|
|
||||||
#### 5.1.3 bool base_frame_parser(base_frame *in_frame, void **out_frame, device *dev)
|
|
||||||
从基础帧中解析出数据,返回`true`为解析成功,`false`为解析失败,需要检查包基础帧数据内容是否错误。
|
|
||||||
| 变量名 | 输入/输出 | 说明 |
|
|
||||||
|:----:|:----:|:----:|
|
|
||||||
| in_frame | 输入 | 基础帧指针 |
|
|
||||||
| out_frame | 输出 | 数据包指针,指向需要从基础帧中解析出的数据包地址 |
|
|
||||||
| dev | 输出 | 接收方的`device`数据结构指针,缓存帧标号和随机数等相关数据 |
|
|
||||||
#### 5.1.4 void ssl_frame_maker(ssl_frame *frame, u8 *data, int data_len)
|
|
||||||
将相关数据打包进非对称双向认证包中。
|
|
||||||
| 变量名 | 输入/输出 | 说明 |
|
|
||||||
|:----:|:----:|:----:|
|
|
||||||
| in_frame | 输出 | 非对称双向认证包指针,指向需要被打包进非对称双向认证包的数据地址 |
|
|
||||||
| data | 输入 | 数据指针 |
|
|
||||||
| data_len | 输入 | 数据长度 |
|
|
||||||
#### 5.1.5 void zigbee_data_encrypt(data_frame *data, crypto_zdata_frame *zdata, bool (* SM4_encrypt)(u8 *key_origin, u32 key_len, u8 *in_origin, u32 in_len, u8 *out, u32 *out_len, bool use_real_cbc),QString en_key = "")
|
|
||||||
对数据进行对称加密。
|
|
||||||
| 变量名 | 输入/输出 | 说明 |
|
|
||||||
|:----:|:----:|:----:|
|
|
||||||
| data | 输入 | 待加密的数据指针 |
|
|
||||||
| zdata | 输出 | 待填充的对称加密数据包指针 |
|
|
||||||
| SM4_encrypt | 输入 | SM4加密算法函数指针 |
|
|
||||||
| en_key | 输入 | 若不提供密钥,则将使用默认密钥进行加密 |
|
|
||||||
#### 5.1.6 bool zigbee_data_dectypt(data_frame *data, crypto_zdata_frame *zdata,bool (* SM4_decrypt)(u8 *key_origin, u32 key_len, u8 *in, u32 in_len, u8 *out, u32 *out_len, bool use_real_cbc),QString en_key = "")
|
|
||||||
对数据进行对称解密,返回`true`代表解密成功,`false`代表解密失败,需要检查输入是否错误。
|
|
||||||
| 变量名 | 输入/输出 | 说明 |
|
|
||||||
|:----:|:----:|:----:|
|
|
||||||
| data | 输出 | 待解密的数据地址指针 |
|
|
||||||
| zdata | 输入 | 收到的对称加密数据包指针 |
|
|
||||||
| SM4_decrypt | 输入 | SM4解密算法函数指针 |
|
|
||||||
| en_key | 输入 | 若不提供密钥,则将使用默认密钥进行解密 |
|
|
||||||
### 5.2 协议认证/验证
|
|
||||||
#### 5.2.1 void HMAC_identify(device *self, device *node, hmac_frame *hframe, void (*sendTonode)(ZigbeeFrame &data), void (*SM3_HMAC)(u8 *key, int keylen,u8 *input, int ilen,u8 output[32]))
|
|
||||||
对接收到的HMAC包进行数据认证。
|
|
||||||
| 变量名 | 输入/输出 | 说明 |
|
|
||||||
|:----:|:----:|:----:|
|
|
||||||
| self | 输出 | 接收方的`device`数据结构指针,缓存帧标号和随机数等相关数据 |
|
|
||||||
| node | 输出 | 发送方的`device`数据结构指针,缓存帧标号和随机数等相关数据 |
|
|
||||||
| hframe | 输入 | 接收到的对称双向认证包指针 |
|
|
||||||
| sendTonode | 输入 | 发送数据函数的指针 |
|
|
||||||
#### 5.2.2 bool data_frame_verify(data_frame *frame)
|
|
||||||
对数据帧进行验证,返回`true`代表包有效,`false`代表包损坏。
|
|
||||||
| 变量名 | 输入/输出 | 说明 |
|
|
||||||
|:----:|:----:|:----:|
|
|
||||||
| frame | 输入 | 待检验的数据帧指针 |
|
|
||||||
#### 5.2.3 void HMAC_changeVerifykey(u8 key[16], device* self, device *node, void (*sendTonode)(ZigbeeFrame &data),bool (* SM4_encrypt)(u8 *key_origin, u32 key_len, u8 *in_origin, u32 in_len, u8 *out, u32 *out_len, bool use_real_cbc))
|
|
||||||
发送对称双向认证密钥更换指令包。
|
|
||||||
| 变量名 | 输入/输出 | 说明 |
|
|
||||||
|:----:|:----:|:----:|
|
|
||||||
| key | 输入 | 新密钥 |
|
|
||||||
| self | 输出 | 接收方的`device`数据结构指针,缓存帧标号和随机数等相关数据 |
|
|
||||||
| node | 输出 | 发送方的`device`数据结构指针,缓存帧标号和随机数等相关数据 |
|
|
||||||
| sendTonode | 输入 | 发送数据函数的指针 |
|
|
||||||
| SM4_encrypt | 输入 | SM4加密算法函数指针 |
|
|
||||||
### 5.3 工具
|
|
||||||
#### 5.3.1 uint16_t crc16_xmodem(const uint8_t *buffer, uint32_t buffer_length)
|
|
||||||
生成CRC16校验码。
|
|
||||||
| 变量名 | 输入/输出 | 说明 |
|
|
||||||
|:----:|:----:|:----:|
|
|
||||||
| buffer | 输入 | 需要生成校验码的数据指针 |
|
|
||||||
| buffer_length | 输入 | 需要生成校验码的数据长度 |
|
|
||||||
#### 5.3.2 bool bytecmp(u8 *a, u8 *b, u16 length)
|
|
||||||
将两个输入进行逐字比较,即`memcmp`,返回`true`代表完全一致,`false`代表存在差异。
|
|
||||||
| 变量名 | 输入/输出 | 说明 |
|
|
||||||
|:----:|:----:|:----:|
|
|
||||||
| a | 输入 | 需对比数据A |
|
|
||||||
| b | 输入 | 需对比数据B |
|
|
||||||
| length | 输入 | 需对比数据长度 |
|
|
|
@ -26,7 +26,7 @@ set(PROJECT_BUNDLE_NAME ${PROJECT_NAME})
|
||||||
set(version_str "${CMAKE_PROJECT_VERSION_MAJOR},${CMAKE_PROJECT_VERSION_MINOR},${CMAKE_PROJECT_VERSION_PATCH}")
|
set(version_str "${CMAKE_PROJECT_VERSION_MAJOR},${CMAKE_PROJECT_VERSION_MINOR},${CMAKE_PROJECT_VERSION_PATCH}")
|
||||||
add_definitions(-DPROTOCOLPARSER_VERSION=${version_str})
|
add_definitions(-DPROTOCOLPARSER_VERSION=${version_str})
|
||||||
|
|
||||||
find_package(Qt6 COMPONENTS Quick SerialPort LinguistTools REQUIRED)
|
find_package(Qt6 COMPONENTS Quick SerialPort LinguistTools WebView REQUIRED)
|
||||||
|
|
||||||
set(
|
set(
|
||||||
sources_files source/main.cpp include/serialportmanager.h source/serialportmanager.cpp include/eventsbus.h source/eventsbus.cpp
|
sources_files source/main.cpp include/serialportmanager.h source/serialportmanager.cpp include/eventsbus.h source/eventsbus.cpp
|
||||||
|
@ -37,6 +37,7 @@ set(
|
||||||
include/serialdataresolver.h source/serialdataresolver.cpp
|
include/serialdataresolver.h source/serialdataresolver.cpp
|
||||||
include/zigbeedataresolver.h source/zigbeedataresolver.cpp
|
include/zigbeedataresolver.h source/zigbeedataresolver.cpp
|
||||||
include/eventbusresolver.h source/eventbusresolver.cpp
|
include/eventbusresolver.h source/eventbusresolver.cpp
|
||||||
|
include/tools.h source/tools.cpp
|
||||||
languages/zh_CN.ts languages/en_US.ts
|
languages/zh_CN.ts languages/en_US.ts
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -46,7 +47,18 @@ set(
|
||||||
qml/components/ZigBeeMessage.qml qml/components/SerialPortAssistant.qml
|
qml/components/ZigBeeMessage.qml qml/components/SerialPortAssistant.qml
|
||||||
qml/components/ZigBeeDataView.qml qml/components/TabBar.qml qml/components/CenterView.qml
|
qml/components/ZigBeeDataView.qml qml/components/TabBar.qml qml/components/CenterView.qml
|
||||||
qml/components/ListTable.qml qml/components/DeviceList.qml qml/components/KeysList.qml
|
qml/components/ListTable.qml qml/components/DeviceList.qml qml/components/KeysList.qml
|
||||||
qml/components/EventsHistoryList.qml qml/components/FrameChooser.qml
|
qml/components/EventsHistoryList.qml qml/components/FrameChooser.qml qml/components/RibbonMarkDownViewer.qml
|
||||||
|
qml/components/HelpView.qml
|
||||||
|
)
|
||||||
|
|
||||||
|
set(js_files js/markdown-it.js js/markdown-it-deflist.js js/markdown-it-emoji.js
|
||||||
|
js/markdown-it-sub.js js/markdown-it-abbr.js js/markdown-it-container.js js/markdown-it-footnote.js
|
||||||
|
js/markdown-it-ins.js js/markdown-it-mark.js js/markdown-it-sup.js js/prism.js
|
||||||
|
)
|
||||||
|
|
||||||
|
set(doc_files ${CMAKE_SOURCE_DIR}/documents/menu.md ${CMAKE_SOURCE_DIR}/documents/protocol.md
|
||||||
|
${CMAKE_SOURCE_DIR}/documents/serialport.md ${CMAKE_SOURCE_DIR}/documents/zigbee.md
|
||||||
|
${CMAKE_SOURCE_DIR}/documents/others.md
|
||||||
)
|
)
|
||||||
|
|
||||||
INCLUDE_DIRECTORIES(dlln3x include)
|
INCLUDE_DIRECTORIES(dlln3x include)
|
||||||
|
@ -97,10 +109,21 @@ foreach(qmlfile ${qml_files})
|
||||||
set_source_files_properties(${qmlfile} PROPERTIES QT_RESOURCE_ALIAS ${fixedfile})
|
set_source_files_properties(${qmlfile} PROPERTIES QT_RESOURCE_ALIAS ${fixedfile})
|
||||||
endforeach(qmlfile)
|
endforeach(qmlfile)
|
||||||
|
|
||||||
|
foreach(jsfile ${js_files})
|
||||||
|
string(REPLACE "js/" "" fixedfile ${jsfile})
|
||||||
|
set_source_files_properties(${jsfile} PROPERTIES QT_RESOURCE_ALIAS ${fixedfile})
|
||||||
|
endforeach(jsfile)
|
||||||
|
|
||||||
|
foreach(docfile ${doc_files})
|
||||||
|
string(REPLACE "${CMAKE_SOURCE_DIR}/" "" fixedfile ${docfile})
|
||||||
|
set_source_files_properties(${docfile} PROPERTIES QT_RESOURCE_ALIAS ${fixedfile})
|
||||||
|
endforeach(docfile)
|
||||||
|
|
||||||
qt_add_qml_module(${PROJECT_NAME}
|
qt_add_qml_module(${PROJECT_NAME}
|
||||||
URI ${PROJECT_NAME}
|
URI ${PROJECT_NAME}
|
||||||
QML_FILES ${qml_files}
|
QML_FILES ${qml_files}
|
||||||
RESOURCE_PREFIX "/qt/qml/"
|
RESOURCE_PREFIX "/qt/qml/"
|
||||||
|
RESOURCES ${js_files} ${doc_files} resources/theme.css resources/default.css resources/prism-light.css resources/prism-dark.css
|
||||||
VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}
|
VERSION ${CMAKE_PROJECT_VERSION_MAJOR}.${CMAKE_PROJECT_VERSION_MINOR}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -125,6 +148,7 @@ if(RIBBONUI_BUILD_STATIC_LIB)
|
||||||
target_link_libraries(${PROJECT_NAME} PRIVATE
|
target_link_libraries(${PROJECT_NAME} PRIVATE
|
||||||
Qt::Quick
|
Qt::Quick
|
||||||
Qt::SerialPort
|
Qt::SerialPort
|
||||||
|
Qt::WebView
|
||||||
RibbonUIplugin
|
RibbonUIplugin
|
||||||
sm_crypto
|
sm_crypto
|
||||||
FramelessHelper::Core
|
FramelessHelper::Core
|
||||||
|
@ -135,6 +159,7 @@ else()
|
||||||
target_link_libraries(${PROJECT_NAME} PRIVATE
|
target_link_libraries(${PROJECT_NAME} PRIVATE
|
||||||
Qt::Quick
|
Qt::Quick
|
||||||
Qt::SerialPort
|
Qt::SerialPort
|
||||||
|
Qt::WebView
|
||||||
RibbonUI
|
RibbonUI
|
||||||
sm_crypto
|
sm_crypto
|
||||||
FramelessHelper::Core
|
FramelessHelper::Core
|
||||||
|
|
|
@ -0,0 +1,30 @@
|
||||||
|
#ifndef TOOLS_H
|
||||||
|
#define TOOLS_H
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
#include <QQmlEngine>
|
||||||
|
#include <QQuickTextDocument>
|
||||||
|
|
||||||
|
class Tools : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
QML_ELEMENT
|
||||||
|
QML_SINGLETON
|
||||||
|
QML_NAMED_ELEMENT(Tools)
|
||||||
|
public:
|
||||||
|
static Tools* instance();
|
||||||
|
static Tools* create(QQmlEngine *qmlEngine, QJSEngine *jsEngine){return instance();}
|
||||||
|
Q_INVOKABLE void setDefaultStyleSheet(QQuickTextDocument *qd, QString path);
|
||||||
|
Q_INVOKABLE QString readAll(QString path);
|
||||||
|
Q_INVOKABLE void setBaseUrl(QQuickTextDocument *qd, QUrl url);
|
||||||
|
Q_INVOKABLE QString fileSuffix(const QString &filePath);
|
||||||
|
Q_INVOKABLE QString fileDir(const QString &filePath);
|
||||||
|
|
||||||
|
private:
|
||||||
|
explicit Tools(QObject *parent = nullptr);
|
||||||
|
Q_DISABLE_COPY_MOVE(Tools)
|
||||||
|
|
||||||
|
signals:
|
||||||
|
};
|
||||||
|
|
||||||
|
#endif // TOOLS_H
|
|
@ -0,0 +1,140 @@
|
||||||
|
/*! markdown-it-abbr 2.0.0 https://github.com/markdown-it/markdown-it-abbr @license MIT */
|
||||||
|
(function(global, factory) {
|
||||||
|
typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory() : typeof define === "function" && define.amd ? define(factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self,
|
||||||
|
global.markdownitAbbr = factory());
|
||||||
|
})(this, (function() {
|
||||||
|
"use strict";
|
||||||
|
// Enclose abbreviations in <abbr> tags
|
||||||
|
|
||||||
|
function abbr_plugin(md) {
|
||||||
|
const escapeRE = md.utils.escapeRE;
|
||||||
|
const arrayReplaceAt = md.utils.arrayReplaceAt;
|
||||||
|
// ASCII characters in Cc, Sc, Sm, Sk categories we should terminate on;
|
||||||
|
// you can check character classes here:
|
||||||
|
// http://www.unicode.org/Public/UNIDATA/UnicodeData.txt
|
||||||
|
const OTHER_CHARS = " \r\n$+<=>^`|~";
|
||||||
|
const UNICODE_PUNCT_RE = md.utils.lib.ucmicro.P.source;
|
||||||
|
const UNICODE_SPACE_RE = md.utils.lib.ucmicro.Z.source;
|
||||||
|
function abbr_def(state, startLine, endLine, silent) {
|
||||||
|
let labelEnd;
|
||||||
|
let pos = state.bMarks[startLine] + state.tShift[startLine];
|
||||||
|
const max = state.eMarks[startLine];
|
||||||
|
if (pos + 2 >= max) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (state.src.charCodeAt(pos++) !== 42 /* * */) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (state.src.charCodeAt(pos++) !== 91 /* [ */) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const labelStart = pos;
|
||||||
|
for (;pos < max; pos++) {
|
||||||
|
const ch = state.src.charCodeAt(pos);
|
||||||
|
if (ch === 91 /* [ */) {
|
||||||
|
return false;
|
||||||
|
} else if (ch === 93 /* ] */) {
|
||||||
|
labelEnd = pos;
|
||||||
|
break;
|
||||||
|
} else if (ch === 92 /* \ */) {
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (labelEnd < 0 || state.src.charCodeAt(labelEnd + 1) !== 58 /* : */) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (silent) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const label = state.src.slice(labelStart, labelEnd).replace(/\\(.)/g, "$1");
|
||||||
|
const title = state.src.slice(labelEnd + 2, max).trim();
|
||||||
|
if (label.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (title.length === 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!state.env.abbreviations) {
|
||||||
|
state.env.abbreviations = {};
|
||||||
|
}
|
||||||
|
// prepend ':' to avoid conflict with Object.prototype members
|
||||||
|
if (typeof state.env.abbreviations[":" + label] === "undefined") {
|
||||||
|
state.env.abbreviations[":" + label] = title;
|
||||||
|
}
|
||||||
|
state.line = startLine + 1;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
function abbr_replace(state) {
|
||||||
|
const blockTokens = state.tokens;
|
||||||
|
if (!state.env.abbreviations) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const regSimple = new RegExp("(?:" + Object.keys(state.env.abbreviations).map((function(x) {
|
||||||
|
return x.substr(1);
|
||||||
|
})).sort((function(a, b) {
|
||||||
|
return b.length - a.length;
|
||||||
|
})).map(escapeRE).join("|") + ")");
|
||||||
|
const regText = "(^|" + UNICODE_PUNCT_RE + "|" + UNICODE_SPACE_RE + "|[" + OTHER_CHARS.split("").map(escapeRE).join("") + "])" + "(" + Object.keys(state.env.abbreviations).map((function(x) {
|
||||||
|
return x.substr(1);
|
||||||
|
})).sort((function(a, b) {
|
||||||
|
return b.length - a.length;
|
||||||
|
})).map(escapeRE).join("|") + ")" + "($|" + UNICODE_PUNCT_RE + "|" + UNICODE_SPACE_RE + "|[" + OTHER_CHARS.split("").map(escapeRE).join("") + "])";
|
||||||
|
const reg = new RegExp(regText, "g");
|
||||||
|
for (let j = 0, l = blockTokens.length; j < l; j++) {
|
||||||
|
if (blockTokens[j].type !== "inline") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let tokens = blockTokens[j].children;
|
||||||
|
// We scan from the end, to keep position when new tags added.
|
||||||
|
for (let i = tokens.length - 1; i >= 0; i--) {
|
||||||
|
const currentToken = tokens[i];
|
||||||
|
if (currentToken.type !== "text") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let pos = 0;
|
||||||
|
const text = currentToken.content;
|
||||||
|
reg.lastIndex = 0;
|
||||||
|
const nodes = [];
|
||||||
|
// fast regexp run to determine whether there are any abbreviated words
|
||||||
|
// in the current token
|
||||||
|
if (!regSimple.test(text)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let m;
|
||||||
|
while (m = reg.exec(text)) {
|
||||||
|
if (m.index > 0 || m[1].length > 0) {
|
||||||
|
const token = new state.Token("text", "", 0);
|
||||||
|
token.content = text.slice(pos, m.index + m[1].length);
|
||||||
|
nodes.push(token);
|
||||||
|
}
|
||||||
|
const token_o = new state.Token("abbr_open", "abbr", 1);
|
||||||
|
token_o.attrs = [ [ "title", state.env.abbreviations[":" + m[2]] ] ];
|
||||||
|
nodes.push(token_o);
|
||||||
|
const token_t = new state.Token("text", "", 0);
|
||||||
|
token_t.content = m[2];
|
||||||
|
nodes.push(token_t);
|
||||||
|
const token_c = new state.Token("abbr_close", "abbr", -1);
|
||||||
|
nodes.push(token_c);
|
||||||
|
reg.lastIndex -= m[3].length;
|
||||||
|
pos = reg.lastIndex;
|
||||||
|
}
|
||||||
|
if (!nodes.length) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (pos < text.length) {
|
||||||
|
const token = new state.Token("text", "", 0);
|
||||||
|
token.content = text.slice(pos);
|
||||||
|
nodes.push(token);
|
||||||
|
}
|
||||||
|
// replace current node
|
||||||
|
blockTokens[j].children = tokens = arrayReplaceAt(tokens, i, nodes);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
md.block.ruler.before("reference", "abbr_def", abbr_def, {
|
||||||
|
alt: [ "paragraph", "reference" ]
|
||||||
|
});
|
||||||
|
md.core.ruler.after("linkify", "abbr_replace", abbr_replace);
|
||||||
|
}
|
||||||
|
return abbr_plugin;
|
||||||
|
}));
|
|
@ -0,0 +1,132 @@
|
||||||
|
/*! markdown-it-container 4.0.0 https://github.com/markdown-it/markdown-it-container @license MIT */
|
||||||
|
(function(global, factory) {
|
||||||
|
typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory() : typeof define === "function" && define.amd ? define(factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self,
|
||||||
|
global.markdownitContainer = factory());
|
||||||
|
})(this, (function() {
|
||||||
|
"use strict";
|
||||||
|
// Process block-level custom containers
|
||||||
|
|
||||||
|
function container_plugin(md, name, options) {
|
||||||
|
// Second param may be useful if you decide
|
||||||
|
// to increase minimal allowed marker length
|
||||||
|
function validateDefault(params /*, markup */) {
|
||||||
|
return params.trim().split(" ", 2)[0] === name;
|
||||||
|
}
|
||||||
|
function renderDefault(tokens, idx, _options, env, slf) {
|
||||||
|
// add a class to the opening tag
|
||||||
|
if (tokens[idx].nesting === 1) {
|
||||||
|
tokens[idx].attrJoin("class", name);
|
||||||
|
}
|
||||||
|
return slf.renderToken(tokens, idx, _options, env, slf);
|
||||||
|
}
|
||||||
|
options = options || {};
|
||||||
|
const min_markers = 3;
|
||||||
|
const marker_str = options.marker || ":";
|
||||||
|
const marker_char = marker_str.charCodeAt(0);
|
||||||
|
const marker_len = marker_str.length;
|
||||||
|
const validate = options.validate || validateDefault;
|
||||||
|
const render = options.render || renderDefault;
|
||||||
|
function container(state, startLine, endLine, silent) {
|
||||||
|
let pos;
|
||||||
|
let auto_closed = false;
|
||||||
|
let start = state.bMarks[startLine] + state.tShift[startLine];
|
||||||
|
let max = state.eMarks[startLine];
|
||||||
|
// Check out the first character quickly,
|
||||||
|
// this should filter out most of non-containers
|
||||||
|
|
||||||
|
if (marker_char !== state.src.charCodeAt(start)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Check out the rest of the marker string
|
||||||
|
|
||||||
|
for (pos = start + 1; pos <= max; pos++) {
|
||||||
|
if (marker_str[(pos - start) % marker_len] !== state.src[pos]) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const marker_count = Math.floor((pos - start) / marker_len);
|
||||||
|
if (marker_count < min_markers) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
pos -= (pos - start) % marker_len;
|
||||||
|
const markup = state.src.slice(start, pos);
|
||||||
|
const params = state.src.slice(pos, max);
|
||||||
|
if (!validate(params, markup)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Since start is found, we can report success here in validation mode
|
||||||
|
|
||||||
|
if (silent) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Search for the end of the block
|
||||||
|
|
||||||
|
let nextLine = startLine;
|
||||||
|
for (;;) {
|
||||||
|
nextLine++;
|
||||||
|
if (nextLine >= endLine) {
|
||||||
|
// unclosed block should be autoclosed by end of document.
|
||||||
|
// also block seems to be autoclosed by end of parent
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
start = state.bMarks[nextLine] + state.tShift[nextLine];
|
||||||
|
max = state.eMarks[nextLine];
|
||||||
|
if (start < max && state.sCount[nextLine] < state.blkIndent) {
|
||||||
|
// non-empty line with negative indent should stop the list:
|
||||||
|
// - ```
|
||||||
|
// test
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (marker_char !== state.src.charCodeAt(start)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (state.sCount[nextLine] - state.blkIndent >= 4) {
|
||||||
|
// closing fence should be indented less than 4 spaces
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
for (pos = start + 1; pos <= max; pos++) {
|
||||||
|
if (marker_str[(pos - start) % marker_len] !== state.src[pos]) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// closing code fence must be at least as long as the opening one
|
||||||
|
if (Math.floor((pos - start) / marker_len) < marker_count) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// make sure tail has spaces only
|
||||||
|
pos -= (pos - start) % marker_len;
|
||||||
|
pos = state.skipSpaces(pos);
|
||||||
|
if (pos < max) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// found!
|
||||||
|
auto_closed = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
const old_parent = state.parentType;
|
||||||
|
const old_line_max = state.lineMax;
|
||||||
|
state.parentType = "container";
|
||||||
|
// this will prevent lazy continuations from ever going past our end marker
|
||||||
|
state.lineMax = nextLine;
|
||||||
|
const token_o = state.push("container_" + name + "_open", "div", 1);
|
||||||
|
token_o.markup = markup;
|
||||||
|
token_o.block = true;
|
||||||
|
token_o.info = params;
|
||||||
|
token_o.map = [ startLine, nextLine ];
|
||||||
|
state.md.block.tokenize(state, startLine + 1, nextLine);
|
||||||
|
const token_c = state.push("container_" + name + "_close", "div", -1);
|
||||||
|
token_c.markup = state.src.slice(start, pos);
|
||||||
|
token_c.block = true;
|
||||||
|
state.parentType = old_parent;
|
||||||
|
state.lineMax = old_line_max;
|
||||||
|
state.line = nextLine + (auto_closed ? 1 : 0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
md.block.ruler.before("fence", "container_" + name, container, {
|
||||||
|
alt: [ "paragraph", "reference", "blockquote", "list" ]
|
||||||
|
});
|
||||||
|
md.renderer.rules["container_" + name + "_open"] = render;
|
||||||
|
md.renderer.rules["container_" + name + "_close"] = render;
|
||||||
|
}
|
||||||
|
return container_plugin;
|
||||||
|
}));
|
|
@ -0,0 +1,203 @@
|
||||||
|
/*! markdown-it-deflist 3.0.0 https://github.com/markdown-it/markdown-it-deflist.git @license MIT */
|
||||||
|
(function(global, factory) {
|
||||||
|
typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory() : typeof define === "function" && define.amd ? define(factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self,
|
||||||
|
global.markdownitDeflist = factory());
|
||||||
|
})(this, (function() {
|
||||||
|
"use strict";
|
||||||
|
// Process definition lists
|
||||||
|
|
||||||
|
function deflist_plugin(md) {
|
||||||
|
const isSpace = md.utils.isSpace;
|
||||||
|
// Search `[:~][\n ]`, returns next pos after marker on success
|
||||||
|
// or -1 on fail.
|
||||||
|
function skipMarker(state, line) {
|
||||||
|
let start = state.bMarks[line] + state.tShift[line];
|
||||||
|
const max = state.eMarks[line];
|
||||||
|
if (start >= max) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
// Check bullet
|
||||||
|
const marker = state.src.charCodeAt(start++);
|
||||||
|
if (marker !== 126 /* ~ */ && marker !== 58 /* : */) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
const pos = state.skipSpaces(start);
|
||||||
|
// require space after ":"
|
||||||
|
if (start === pos) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
// no empty definitions, e.g. " : "
|
||||||
|
if (pos >= max) {
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
return start;
|
||||||
|
}
|
||||||
|
function markTightParagraphs(state, idx) {
|
||||||
|
const level = state.level + 2;
|
||||||
|
for (let i = idx + 2, l = state.tokens.length - 2; i < l; i++) {
|
||||||
|
if (state.tokens[i].level === level && state.tokens[i].type === "paragraph_open") {
|
||||||
|
state.tokens[i + 2].hidden = true;
|
||||||
|
state.tokens[i].hidden = true;
|
||||||
|
i += 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function deflist(state, startLine, endLine, silent) {
|
||||||
|
if (silent) {
|
||||||
|
// quirk: validation mode validates a dd block only, not a whole deflist
|
||||||
|
if (state.ddIndent < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return skipMarker(state, startLine) >= 0;
|
||||||
|
}
|
||||||
|
let nextLine = startLine + 1;
|
||||||
|
if (nextLine >= endLine) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (state.isEmpty(nextLine)) {
|
||||||
|
nextLine++;
|
||||||
|
if (nextLine >= endLine) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (state.sCount[nextLine] < state.blkIndent) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let contentStart = skipMarker(state, nextLine);
|
||||||
|
if (contentStart < 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Start list
|
||||||
|
const listTokIdx = state.tokens.length;
|
||||||
|
let tight = true;
|
||||||
|
const token_dl_o = state.push("dl_open", "dl", 1);
|
||||||
|
const listLines = [ startLine, 0 ];
|
||||||
|
token_dl_o.map = listLines;
|
||||||
|
|
||||||
|
// Iterate list items
|
||||||
|
|
||||||
|
let dtLine = startLine;
|
||||||
|
let ddLine = nextLine;
|
||||||
|
// One definition list can contain multiple DTs,
|
||||||
|
// and one DT can be followed by multiple DDs.
|
||||||
|
|
||||||
|
// Thus, there is two loops here, and label is
|
||||||
|
// needed to break out of the second one
|
||||||
|
|
||||||
|
/* eslint no-labels:0,block-scoped-var:0 */ OUTER: for (;;) {
|
||||||
|
let prevEmptyEnd = false;
|
||||||
|
const token_dt_o = state.push("dt_open", "dt", 1);
|
||||||
|
token_dt_o.map = [ dtLine, dtLine ];
|
||||||
|
const token_i = state.push("inline", "", 0);
|
||||||
|
token_i.map = [ dtLine, dtLine ];
|
||||||
|
token_i.content = state.getLines(dtLine, dtLine + 1, state.blkIndent, false).trim();
|
||||||
|
token_i.children = [];
|
||||||
|
state.push("dt_close", "dt", -1);
|
||||||
|
for (;;) {
|
||||||
|
const token_dd_o = state.push("dd_open", "dd", 1);
|
||||||
|
const itemLines = [ nextLine, 0 ];
|
||||||
|
token_dd_o.map = itemLines;
|
||||||
|
let pos = contentStart;
|
||||||
|
const max = state.eMarks[ddLine];
|
||||||
|
let offset = state.sCount[ddLine] + contentStart - (state.bMarks[ddLine] + state.tShift[ddLine]);
|
||||||
|
while (pos < max) {
|
||||||
|
const ch = state.src.charCodeAt(pos);
|
||||||
|
if (isSpace(ch)) {
|
||||||
|
if (ch === 9) {
|
||||||
|
offset += 4 - offset % 4;
|
||||||
|
} else {
|
||||||
|
offset++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
contentStart = pos;
|
||||||
|
const oldTight = state.tight;
|
||||||
|
const oldDDIndent = state.ddIndent;
|
||||||
|
const oldIndent = state.blkIndent;
|
||||||
|
const oldTShift = state.tShift[ddLine];
|
||||||
|
const oldSCount = state.sCount[ddLine];
|
||||||
|
const oldParentType = state.parentType;
|
||||||
|
state.blkIndent = state.ddIndent = state.sCount[ddLine] + 2;
|
||||||
|
state.tShift[ddLine] = contentStart - state.bMarks[ddLine];
|
||||||
|
state.sCount[ddLine] = offset;
|
||||||
|
state.tight = true;
|
||||||
|
state.parentType = "deflist";
|
||||||
|
state.md.block.tokenize(state, ddLine, endLine, true);
|
||||||
|
// If any of list item is tight, mark list as tight
|
||||||
|
if (!state.tight || prevEmptyEnd) {
|
||||||
|
tight = false;
|
||||||
|
}
|
||||||
|
// Item become loose if finish with empty line,
|
||||||
|
// but we should filter last element, because it means list finish
|
||||||
|
prevEmptyEnd = state.line - ddLine > 1 && state.isEmpty(state.line - 1);
|
||||||
|
state.tShift[ddLine] = oldTShift;
|
||||||
|
state.sCount[ddLine] = oldSCount;
|
||||||
|
state.tight = oldTight;
|
||||||
|
state.parentType = oldParentType;
|
||||||
|
state.blkIndent = oldIndent;
|
||||||
|
state.ddIndent = oldDDIndent;
|
||||||
|
state.push("dd_close", "dd", -1);
|
||||||
|
itemLines[1] = nextLine = state.line;
|
||||||
|
if (nextLine >= endLine) {
|
||||||
|
break OUTER;
|
||||||
|
}
|
||||||
|
if (state.sCount[nextLine] < state.blkIndent) {
|
||||||
|
break OUTER;
|
||||||
|
}
|
||||||
|
contentStart = skipMarker(state, nextLine);
|
||||||
|
if (contentStart < 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ddLine = nextLine;
|
||||||
|
// go to the next loop iteration:
|
||||||
|
// insert DD tag and repeat checking
|
||||||
|
}
|
||||||
|
if (nextLine >= endLine) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
dtLine = nextLine;
|
||||||
|
if (state.isEmpty(dtLine)) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (state.sCount[dtLine] < state.blkIndent) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ddLine = dtLine + 1;
|
||||||
|
if (ddLine >= endLine) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (state.isEmpty(ddLine)) {
|
||||||
|
ddLine++;
|
||||||
|
}
|
||||||
|
if (ddLine >= endLine) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (state.sCount[ddLine] < state.blkIndent) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
contentStart = skipMarker(state, ddLine);
|
||||||
|
if (contentStart < 0) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// go to the next loop iteration:
|
||||||
|
// insert DT and DD tags and repeat checking
|
||||||
|
}
|
||||||
|
// Finilize list
|
||||||
|
state.push("dl_close", "dl", -1);
|
||||||
|
listLines[1] = nextLine;
|
||||||
|
state.line = nextLine;
|
||||||
|
// mark paragraphs tight if needed
|
||||||
|
if (tight) {
|
||||||
|
markTightParagraphs(state, listTokIdx);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
md.block.ruler.before("paragraph", "deflist", deflist, {
|
||||||
|
alt: [ "paragraph", "reference", "blockquote" ]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return deflist_plugin;
|
||||||
|
}));
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,299 @@
|
||||||
|
/*! markdown-it-footnote 4.0.0 https://github.com/markdown-it/markdown-it-footnote @license MIT */
|
||||||
|
(function(global, factory) {
|
||||||
|
typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory() : typeof define === "function" && define.amd ? define(factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self,
|
||||||
|
global.markdownitFootnote = factory());
|
||||||
|
})(this, (function() {
|
||||||
|
"use strict";
|
||||||
|
// Process footnotes
|
||||||
|
|
||||||
|
/// /////////////////////////////////////////////////////////////////////////////
|
||||||
|
// Renderer partials
|
||||||
|
function render_footnote_anchor_name(tokens, idx, options, env /*, slf */) {
|
||||||
|
const n = Number(tokens[idx].meta.id + 1).toString();
|
||||||
|
let prefix = "";
|
||||||
|
if (typeof env.docId === "string") prefix = `-${env.docId}-`;
|
||||||
|
return prefix + n;
|
||||||
|
}
|
||||||
|
function render_footnote_caption(tokens, idx /*, options, env, slf */) {
|
||||||
|
let n = Number(tokens[idx].meta.id + 1).toString();
|
||||||
|
if (tokens[idx].meta.subId > 0) n += `:${tokens[idx].meta.subId}`;
|
||||||
|
return `[${n}]`;
|
||||||
|
}
|
||||||
|
function render_footnote_ref(tokens, idx, options, env, slf) {
|
||||||
|
const id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf);
|
||||||
|
const caption = slf.rules.footnote_caption(tokens, idx, options, env, slf);
|
||||||
|
let refid = id;
|
||||||
|
if (tokens[idx].meta.subId > 0) refid += `:${tokens[idx].meta.subId}`;
|
||||||
|
return `<sup class="footnote-ref"><a href="#fn${id}" id="fnref${refid}">${caption}</a></sup>`;
|
||||||
|
}
|
||||||
|
function render_footnote_block_open(tokens, idx, options) {
|
||||||
|
return (options.xhtmlOut ? '<hr class="footnotes-sep" />\n' : '<hr class="footnotes-sep">\n') + '<section class="footnotes">\n' + '<ol class="footnotes-list">\n';
|
||||||
|
}
|
||||||
|
function render_footnote_block_close() {
|
||||||
|
return "</ol>\n</section>\n";
|
||||||
|
}
|
||||||
|
function render_footnote_open(tokens, idx, options, env, slf) {
|
||||||
|
let id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf);
|
||||||
|
if (tokens[idx].meta.subId > 0) id += `:${tokens[idx].meta.subId}`;
|
||||||
|
return `<li id="fn${id}" class="footnote-item">`;
|
||||||
|
}
|
||||||
|
function render_footnote_close() {
|
||||||
|
return "</li>\n";
|
||||||
|
}
|
||||||
|
function render_footnote_anchor(tokens, idx, options, env, slf) {
|
||||||
|
let id = slf.rules.footnote_anchor_name(tokens, idx, options, env, slf);
|
||||||
|
if (tokens[idx].meta.subId > 0) id += `:${tokens[idx].meta.subId}`
|
||||||
|
/* ↩ with escape code to prevent display as Apple Emoji on iOS */;
|
||||||
|
return ` <a href="#fnref${id}" class="footnote-backref">\u21a9\ufe0e</a>`;
|
||||||
|
}
|
||||||
|
function footnote_plugin(md) {
|
||||||
|
const parseLinkLabel = md.helpers.parseLinkLabel;
|
||||||
|
const isSpace = md.utils.isSpace;
|
||||||
|
md.renderer.rules.footnote_ref = render_footnote_ref;
|
||||||
|
md.renderer.rules.footnote_block_open = render_footnote_block_open;
|
||||||
|
md.renderer.rules.footnote_block_close = render_footnote_block_close;
|
||||||
|
md.renderer.rules.footnote_open = render_footnote_open;
|
||||||
|
md.renderer.rules.footnote_close = render_footnote_close;
|
||||||
|
md.renderer.rules.footnote_anchor = render_footnote_anchor;
|
||||||
|
// helpers (only used in other rules, no tokens are attached to those)
|
||||||
|
md.renderer.rules.footnote_caption = render_footnote_caption;
|
||||||
|
md.renderer.rules.footnote_anchor_name = render_footnote_anchor_name;
|
||||||
|
// Process footnote block definition
|
||||||
|
function footnote_def(state, startLine, endLine, silent) {
|
||||||
|
const start = state.bMarks[startLine] + state.tShift[startLine];
|
||||||
|
const max = state.eMarks[startLine];
|
||||||
|
// line should be at least 5 chars - "[^x]:"
|
||||||
|
if (start + 4 > max) return false;
|
||||||
|
if (state.src.charCodeAt(start) !== 91 /* [ */) return false;
|
||||||
|
if (state.src.charCodeAt(start + 1) !== 94 /* ^ */) return false;
|
||||||
|
let pos;
|
||||||
|
for (pos = start + 2; pos < max; pos++) {
|
||||||
|
if (state.src.charCodeAt(pos) === 32) return false;
|
||||||
|
if (state.src.charCodeAt(pos) === 93 /* ] */) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pos === start + 2) return false;
|
||||||
|
// no empty footnote labels
|
||||||
|
if (pos + 1 >= max || state.src.charCodeAt(++pos) !== 58 /* : */) return false;
|
||||||
|
if (silent) return true;
|
||||||
|
pos++;
|
||||||
|
if (!state.env.footnotes) state.env.footnotes = {};
|
||||||
|
if (!state.env.footnotes.refs) state.env.footnotes.refs = {};
|
||||||
|
const label = state.src.slice(start + 2, pos - 2);
|
||||||
|
state.env.footnotes.refs[`:${label}`] = -1;
|
||||||
|
const token_fref_o = new state.Token("footnote_reference_open", "", 1);
|
||||||
|
token_fref_o.meta = {
|
||||||
|
label: label
|
||||||
|
};
|
||||||
|
token_fref_o.level = state.level++;
|
||||||
|
state.tokens.push(token_fref_o);
|
||||||
|
const oldBMark = state.bMarks[startLine];
|
||||||
|
const oldTShift = state.tShift[startLine];
|
||||||
|
const oldSCount = state.sCount[startLine];
|
||||||
|
const oldParentType = state.parentType;
|
||||||
|
const posAfterColon = pos;
|
||||||
|
const initial = state.sCount[startLine] + pos - (state.bMarks[startLine] + state.tShift[startLine]);
|
||||||
|
let offset = initial;
|
||||||
|
while (pos < max) {
|
||||||
|
const ch = state.src.charCodeAt(pos);
|
||||||
|
if (isSpace(ch)) {
|
||||||
|
if (ch === 9) {
|
||||||
|
offset += 4 - offset % 4;
|
||||||
|
} else {
|
||||||
|
offset++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
state.tShift[startLine] = pos - posAfterColon;
|
||||||
|
state.sCount[startLine] = offset - initial;
|
||||||
|
state.bMarks[startLine] = posAfterColon;
|
||||||
|
state.blkIndent += 4;
|
||||||
|
state.parentType = "footnote";
|
||||||
|
if (state.sCount[startLine] < state.blkIndent) {
|
||||||
|
state.sCount[startLine] += state.blkIndent;
|
||||||
|
}
|
||||||
|
state.md.block.tokenize(state, startLine, endLine, true);
|
||||||
|
state.parentType = oldParentType;
|
||||||
|
state.blkIndent -= 4;
|
||||||
|
state.tShift[startLine] = oldTShift;
|
||||||
|
state.sCount[startLine] = oldSCount;
|
||||||
|
state.bMarks[startLine] = oldBMark;
|
||||||
|
const token_fref_c = new state.Token("footnote_reference_close", "", -1);
|
||||||
|
token_fref_c.level = --state.level;
|
||||||
|
state.tokens.push(token_fref_c);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Process inline footnotes (^[...])
|
||||||
|
function footnote_inline(state, silent) {
|
||||||
|
const max = state.posMax;
|
||||||
|
const start = state.pos;
|
||||||
|
if (start + 2 >= max) return false;
|
||||||
|
if (state.src.charCodeAt(start) !== 94 /* ^ */) return false;
|
||||||
|
if (state.src.charCodeAt(start + 1) !== 91 /* [ */) return false;
|
||||||
|
const labelStart = start + 2;
|
||||||
|
const labelEnd = parseLinkLabel(state, start + 1);
|
||||||
|
// parser failed to find ']', so it's not a valid note
|
||||||
|
if (labelEnd < 0) return false;
|
||||||
|
// We found the end of the link, and know for a fact it's a valid link;
|
||||||
|
// so all that's left to do is to call tokenizer.
|
||||||
|
|
||||||
|
if (!silent) {
|
||||||
|
if (!state.env.footnotes) state.env.footnotes = {};
|
||||||
|
if (!state.env.footnotes.list) state.env.footnotes.list = [];
|
||||||
|
const footnoteId = state.env.footnotes.list.length;
|
||||||
|
const tokens = [];
|
||||||
|
state.md.inline.parse(state.src.slice(labelStart, labelEnd), state.md, state.env, tokens);
|
||||||
|
const token = state.push("footnote_ref", "", 0);
|
||||||
|
token.meta = {
|
||||||
|
id: footnoteId
|
||||||
|
};
|
||||||
|
state.env.footnotes.list[footnoteId] = {
|
||||||
|
content: state.src.slice(labelStart, labelEnd),
|
||||||
|
tokens: tokens
|
||||||
|
};
|
||||||
|
}
|
||||||
|
state.pos = labelEnd + 1;
|
||||||
|
state.posMax = max;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Process footnote references ([^...])
|
||||||
|
function footnote_ref(state, silent) {
|
||||||
|
const max = state.posMax;
|
||||||
|
const start = state.pos;
|
||||||
|
// should be at least 4 chars - "[^x]"
|
||||||
|
if (start + 3 > max) return false;
|
||||||
|
if (!state.env.footnotes || !state.env.footnotes.refs) return false;
|
||||||
|
if (state.src.charCodeAt(start) !== 91 /* [ */) return false;
|
||||||
|
if (state.src.charCodeAt(start + 1) !== 94 /* ^ */) return false;
|
||||||
|
let pos;
|
||||||
|
for (pos = start + 2; pos < max; pos++) {
|
||||||
|
if (state.src.charCodeAt(pos) === 32) return false;
|
||||||
|
if (state.src.charCodeAt(pos) === 10) return false;
|
||||||
|
if (state.src.charCodeAt(pos) === 93 /* ] */) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (pos === start + 2) return false;
|
||||||
|
// no empty footnote labels
|
||||||
|
if (pos >= max) return false;
|
||||||
|
pos++;
|
||||||
|
const label = state.src.slice(start + 2, pos - 1);
|
||||||
|
if (typeof state.env.footnotes.refs[`:${label}`] === "undefined") return false;
|
||||||
|
if (!silent) {
|
||||||
|
if (!state.env.footnotes.list) state.env.footnotes.list = [];
|
||||||
|
let footnoteId;
|
||||||
|
if (state.env.footnotes.refs[`:${label}`] < 0) {
|
||||||
|
footnoteId = state.env.footnotes.list.length;
|
||||||
|
state.env.footnotes.list[footnoteId] = {
|
||||||
|
label: label,
|
||||||
|
count: 0
|
||||||
|
};
|
||||||
|
state.env.footnotes.refs[`:${label}`] = footnoteId;
|
||||||
|
} else {
|
||||||
|
footnoteId = state.env.footnotes.refs[`:${label}`];
|
||||||
|
}
|
||||||
|
const footnoteSubId = state.env.footnotes.list[footnoteId].count;
|
||||||
|
state.env.footnotes.list[footnoteId].count++;
|
||||||
|
const token = state.push("footnote_ref", "", 0);
|
||||||
|
token.meta = {
|
||||||
|
id: footnoteId,
|
||||||
|
subId: footnoteSubId,
|
||||||
|
label: label
|
||||||
|
};
|
||||||
|
}
|
||||||
|
state.pos = pos;
|
||||||
|
state.posMax = max;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Glue footnote tokens to end of token stream
|
||||||
|
function footnote_tail(state) {
|
||||||
|
let tokens;
|
||||||
|
let current;
|
||||||
|
let currentLabel;
|
||||||
|
let insideRef = false;
|
||||||
|
const refTokens = {};
|
||||||
|
if (!state.env.footnotes) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.tokens = state.tokens.filter((function(tok) {
|
||||||
|
if (tok.type === "footnote_reference_open") {
|
||||||
|
insideRef = true;
|
||||||
|
current = [];
|
||||||
|
currentLabel = tok.meta.label;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (tok.type === "footnote_reference_close") {
|
||||||
|
insideRef = false;
|
||||||
|
// prepend ':' to avoid conflict with Object.prototype members
|
||||||
|
refTokens[":" + currentLabel] = current;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (insideRef) {
|
||||||
|
current.push(tok);
|
||||||
|
}
|
||||||
|
return !insideRef;
|
||||||
|
}));
|
||||||
|
if (!state.env.footnotes.list) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const list = state.env.footnotes.list;
|
||||||
|
state.tokens.push(new state.Token("footnote_block_open", "", 1));
|
||||||
|
for (let i = 0, l = list.length; i < l; i++) {
|
||||||
|
const token_fo = new state.Token("footnote_open", "", 1);
|
||||||
|
token_fo.meta = {
|
||||||
|
id: i,
|
||||||
|
label: list[i].label
|
||||||
|
};
|
||||||
|
state.tokens.push(token_fo);
|
||||||
|
if (list[i].tokens) {
|
||||||
|
tokens = [];
|
||||||
|
const token_po = new state.Token("paragraph_open", "p", 1);
|
||||||
|
token_po.block = true;
|
||||||
|
tokens.push(token_po);
|
||||||
|
const token_i = new state.Token("inline", "", 0);
|
||||||
|
token_i.children = list[i].tokens;
|
||||||
|
token_i.content = list[i].content;
|
||||||
|
tokens.push(token_i);
|
||||||
|
const token_pc = new state.Token("paragraph_close", "p", -1);
|
||||||
|
token_pc.block = true;
|
||||||
|
tokens.push(token_pc);
|
||||||
|
} else if (list[i].label) {
|
||||||
|
tokens = refTokens[`:${list[i].label}`];
|
||||||
|
}
|
||||||
|
if (tokens) state.tokens = state.tokens.concat(tokens);
|
||||||
|
let lastParagraph;
|
||||||
|
if (state.tokens[state.tokens.length - 1].type === "paragraph_close") {
|
||||||
|
lastParagraph = state.tokens.pop();
|
||||||
|
} else {
|
||||||
|
lastParagraph = null;
|
||||||
|
}
|
||||||
|
const t = list[i].count > 0 ? list[i].count : 1;
|
||||||
|
for (let j = 0; j < t; j++) {
|
||||||
|
const token_a = new state.Token("footnote_anchor", "", 0);
|
||||||
|
token_a.meta = {
|
||||||
|
id: i,
|
||||||
|
subId: j,
|
||||||
|
label: list[i].label
|
||||||
|
};
|
||||||
|
state.tokens.push(token_a);
|
||||||
|
}
|
||||||
|
if (lastParagraph) {
|
||||||
|
state.tokens.push(lastParagraph);
|
||||||
|
}
|
||||||
|
state.tokens.push(new state.Token("footnote_close", "", -1));
|
||||||
|
}
|
||||||
|
state.tokens.push(new state.Token("footnote_block_close", "", -1));
|
||||||
|
}
|
||||||
|
md.block.ruler.before("reference", "footnote_def", footnote_def, {
|
||||||
|
alt: [ "paragraph", "reference" ]
|
||||||
|
});
|
||||||
|
md.inline.ruler.after("image", "footnote_inline", footnote_inline);
|
||||||
|
md.inline.ruler.after("footnote_inline", "footnote_ref", footnote_ref);
|
||||||
|
md.core.ruler.after("inline", "footnote_tail", footnote_tail);
|
||||||
|
}
|
||||||
|
return footnote_plugin;
|
||||||
|
}));
|
|
@ -0,0 +1,114 @@
|
||||||
|
/*! markdown-it-ins 4.0.0 https://github.com/markdown-it/markdown-it-ins @license MIT */
|
||||||
|
(function(global, factory) {
|
||||||
|
typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory() : typeof define === "function" && define.amd ? define(factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self,
|
||||||
|
global.markdownitIns = factory());
|
||||||
|
})(this, (function() {
|
||||||
|
"use strict";
|
||||||
|
function ins_plugin(md) {
|
||||||
|
// Insert each marker as a separate text token, and add it to delimiter list
|
||||||
|
function tokenize(state, silent) {
|
||||||
|
const start = state.pos;
|
||||||
|
const marker = state.src.charCodeAt(start);
|
||||||
|
if (silent) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (marker !== 43 /* + */) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const scanned = state.scanDelims(state.pos, true);
|
||||||
|
let len = scanned.length;
|
||||||
|
const ch = String.fromCharCode(marker);
|
||||||
|
if (len < 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (len % 2) {
|
||||||
|
const token = state.push("text", "", 0);
|
||||||
|
token.content = ch;
|
||||||
|
len--;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < len; i += 2) {
|
||||||
|
const token = state.push("text", "", 0);
|
||||||
|
token.content = ch + ch;
|
||||||
|
if (!scanned.can_open && !scanned.can_close) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
state.delimiters.push({
|
||||||
|
marker: marker,
|
||||||
|
length: 0,
|
||||||
|
// disable "rule of 3" length checks meant for emphasis
|
||||||
|
jump: i / 2,
|
||||||
|
// 1 delimiter = 2 characters
|
||||||
|
token: state.tokens.length - 1,
|
||||||
|
end: -1,
|
||||||
|
open: scanned.can_open,
|
||||||
|
close: scanned.can_close
|
||||||
|
});
|
||||||
|
}
|
||||||
|
state.pos += scanned.length;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Walk through delimiter list and replace text tokens with tags
|
||||||
|
|
||||||
|
function postProcess(state, delimiters) {
|
||||||
|
let token;
|
||||||
|
const loneMarkers = [];
|
||||||
|
const max = delimiters.length;
|
||||||
|
for (let i = 0; i < max; i++) {
|
||||||
|
const startDelim = delimiters[i];
|
||||||
|
if (startDelim.marker !== 43 /* + */) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (startDelim.end === -1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const endDelim = delimiters[startDelim.end];
|
||||||
|
token = state.tokens[startDelim.token];
|
||||||
|
token.type = "ins_open";
|
||||||
|
token.tag = "ins";
|
||||||
|
token.nesting = 1;
|
||||||
|
token.markup = "++";
|
||||||
|
token.content = "";
|
||||||
|
token = state.tokens[endDelim.token];
|
||||||
|
token.type = "ins_close";
|
||||||
|
token.tag = "ins";
|
||||||
|
token.nesting = -1;
|
||||||
|
token.markup = "++";
|
||||||
|
token.content = "";
|
||||||
|
if (state.tokens[endDelim.token - 1].type === "text" && state.tokens[endDelim.token - 1].content === "+") {
|
||||||
|
loneMarkers.push(endDelim.token - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If a marker sequence has an odd number of characters, it's splitted
|
||||||
|
// like this: `~~~~~` -> `~` + `~~` + `~~`, leaving one marker at the
|
||||||
|
// start of the sequence.
|
||||||
|
|
||||||
|
// So, we have to move all those markers after subsequent s_close tags.
|
||||||
|
|
||||||
|
while (loneMarkers.length) {
|
||||||
|
const i = loneMarkers.pop();
|
||||||
|
let j = i + 1;
|
||||||
|
while (j < state.tokens.length && state.tokens[j].type === "ins_close") {
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
j--;
|
||||||
|
if (i !== j) {
|
||||||
|
token = state.tokens[j];
|
||||||
|
state.tokens[j] = state.tokens[i];
|
||||||
|
state.tokens[i] = token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
md.inline.ruler.before("emphasis", "ins", tokenize);
|
||||||
|
md.inline.ruler2.before("emphasis", "ins", (function(state) {
|
||||||
|
const tokens_meta = state.tokens_meta;
|
||||||
|
const max = (state.tokens_meta || []).length;
|
||||||
|
postProcess(state, state.delimiters);
|
||||||
|
for (let curr = 0; curr < max; curr++) {
|
||||||
|
if (tokens_meta[curr] && tokens_meta[curr].delimiters) {
|
||||||
|
postProcess(state, tokens_meta[curr].delimiters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return ins_plugin;
|
||||||
|
}));
|
|
@ -0,0 +1,114 @@
|
||||||
|
/*! markdown-it-mark 4.0.0 https://github.com/markdown-it/markdown-it-mark @license MIT */
|
||||||
|
(function(global, factory) {
|
||||||
|
typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory() : typeof define === "function" && define.amd ? define(factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self,
|
||||||
|
global.markdownitMark = factory());
|
||||||
|
})(this, (function() {
|
||||||
|
"use strict";
|
||||||
|
function ins_plugin(md) {
|
||||||
|
// Insert each marker as a separate text token, and add it to delimiter list
|
||||||
|
function tokenize(state, silent) {
|
||||||
|
const start = state.pos;
|
||||||
|
const marker = state.src.charCodeAt(start);
|
||||||
|
if (silent) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (marker !== 61 /* = */) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const scanned = state.scanDelims(state.pos, true);
|
||||||
|
let len = scanned.length;
|
||||||
|
const ch = String.fromCharCode(marker);
|
||||||
|
if (len < 2) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (len % 2) {
|
||||||
|
const token = state.push("text", "", 0);
|
||||||
|
token.content = ch;
|
||||||
|
len--;
|
||||||
|
}
|
||||||
|
for (let i = 0; i < len; i += 2) {
|
||||||
|
const token = state.push("text", "", 0);
|
||||||
|
token.content = ch + ch;
|
||||||
|
if (!scanned.can_open && !scanned.can_close) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
state.delimiters.push({
|
||||||
|
marker: marker,
|
||||||
|
length: 0,
|
||||||
|
// disable "rule of 3" length checks meant for emphasis
|
||||||
|
jump: i / 2,
|
||||||
|
// 1 delimiter = 2 characters
|
||||||
|
token: state.tokens.length - 1,
|
||||||
|
end: -1,
|
||||||
|
open: scanned.can_open,
|
||||||
|
close: scanned.can_close
|
||||||
|
});
|
||||||
|
}
|
||||||
|
state.pos += scanned.length;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Walk through delimiter list and replace text tokens with tags
|
||||||
|
|
||||||
|
function postProcess(state, delimiters) {
|
||||||
|
const loneMarkers = [];
|
||||||
|
const max = delimiters.length;
|
||||||
|
for (let i = 0; i < max; i++) {
|
||||||
|
const startDelim = delimiters[i];
|
||||||
|
if (startDelim.marker !== 61 /* = */) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (startDelim.end === -1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const endDelim = delimiters[startDelim.end];
|
||||||
|
const token_o = state.tokens[startDelim.token];
|
||||||
|
token_o.type = "mark_open";
|
||||||
|
token_o.tag = "mark";
|
||||||
|
token_o.nesting = 1;
|
||||||
|
token_o.markup = "==";
|
||||||
|
token_o.content = "";
|
||||||
|
const token_c = state.tokens[endDelim.token];
|
||||||
|
token_c.type = "mark_close";
|
||||||
|
token_c.tag = "mark";
|
||||||
|
token_c.nesting = -1;
|
||||||
|
token_c.markup = "==";
|
||||||
|
token_c.content = "";
|
||||||
|
if (state.tokens[endDelim.token - 1].type === "text" && state.tokens[endDelim.token - 1].content === "=") {
|
||||||
|
loneMarkers.push(endDelim.token - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If a marker sequence has an odd number of characters, it's splitted
|
||||||
|
// like this: `~~~~~` -> `~` + `~~` + `~~`, leaving one marker at the
|
||||||
|
// start of the sequence.
|
||||||
|
|
||||||
|
// So, we have to move all those markers after subsequent s_close tags.
|
||||||
|
|
||||||
|
while (loneMarkers.length) {
|
||||||
|
const i = loneMarkers.pop();
|
||||||
|
let j = i + 1;
|
||||||
|
while (j < state.tokens.length && state.tokens[j].type === "mark_close") {
|
||||||
|
j++;
|
||||||
|
}
|
||||||
|
j--;
|
||||||
|
if (i !== j) {
|
||||||
|
const token = state.tokens[j];
|
||||||
|
state.tokens[j] = state.tokens[i];
|
||||||
|
state.tokens[i] = token;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
md.inline.ruler.before("emphasis", "mark", tokenize);
|
||||||
|
md.inline.ruler2.before("emphasis", "mark", (function(state) {
|
||||||
|
let curr;
|
||||||
|
const tokens_meta = state.tokens_meta;
|
||||||
|
const max = (state.tokens_meta || []).length;
|
||||||
|
postProcess(state, state.delimiters);
|
||||||
|
for (curr = 0; curr < max; curr++) {
|
||||||
|
if (tokens_meta[curr] && tokens_meta[curr].delimiters) {
|
||||||
|
postProcess(state, tokens_meta[curr].delimiters);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
return ins_plugin;
|
||||||
|
}));
|
|
@ -0,0 +1,60 @@
|
||||||
|
/*! markdown-it-sub 2.0.0 https://github.com/markdown-it/markdown-it-sub @license MIT */
|
||||||
|
(function(global, factory) {
|
||||||
|
typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory() : typeof define === "function" && define.amd ? define(factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self,
|
||||||
|
global.markdownitSub = factory());
|
||||||
|
})(this, (function() {
|
||||||
|
"use strict";
|
||||||
|
// Process ~subscript~
|
||||||
|
// same as UNESCAPE_MD_RE plus a space
|
||||||
|
const UNESCAPE_RE = /\\([ \\!"#$%&'()*+,./:;<=>?@[\]^_`{|}~-])/g;
|
||||||
|
function subscript(state, silent) {
|
||||||
|
const max = state.posMax;
|
||||||
|
const start = state.pos;
|
||||||
|
if (state.src.charCodeAt(start) !== 126 /* ~ */) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (silent) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// don't run any pairs in validation mode
|
||||||
|
if (start + 2 >= max) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
state.pos = start + 1;
|
||||||
|
let found = false;
|
||||||
|
while (state.pos < max) {
|
||||||
|
if (state.src.charCodeAt(state.pos) === 126 /* ~ */) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
state.md.inline.skipToken(state);
|
||||||
|
}
|
||||||
|
if (!found || start + 1 === state.pos) {
|
||||||
|
state.pos = start;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const content = state.src.slice(start + 1, state.pos);
|
||||||
|
// don't allow unescaped spaces/newlines inside
|
||||||
|
if (content.match(/(^|[^\\])(\\\\)*\s/)) {
|
||||||
|
state.pos = start;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// found!
|
||||||
|
state.posMax = state.pos;
|
||||||
|
state.pos = start + 1;
|
||||||
|
// Earlier we checked !silent, but this implementation does not need it
|
||||||
|
const token_so = state.push("sub_open", "sub", 1);
|
||||||
|
token_so.markup = "~";
|
||||||
|
const token_t = state.push("text", "", 0);
|
||||||
|
token_t.content = content.replace(UNESCAPE_RE, "$1");
|
||||||
|
const token_sc = state.push("sub_close", "sub", -1);
|
||||||
|
token_sc.markup = "~";
|
||||||
|
state.pos = state.posMax + 1;
|
||||||
|
state.posMax = max;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
function sub_plugin(md) {
|
||||||
|
md.inline.ruler.after("emphasis", "sub", subscript);
|
||||||
|
}
|
||||||
|
return sub_plugin;
|
||||||
|
}));
|
|
@ -0,0 +1,60 @@
|
||||||
|
/*! markdown-it-sup 2.0.0 https://github.com/markdown-it/markdown-it-sup @license MIT */
|
||||||
|
(function(global, factory) {
|
||||||
|
typeof exports === "object" && typeof module !== "undefined" ? module.exports = factory() : typeof define === "function" && define.amd ? define(factory) : (global = typeof globalThis !== "undefined" ? globalThis : global || self,
|
||||||
|
global.markdownitSup = factory());
|
||||||
|
})(this, (function() {
|
||||||
|
"use strict";
|
||||||
|
// Process ^superscript^
|
||||||
|
// same as UNESCAPE_MD_RE plus a space
|
||||||
|
const UNESCAPE_RE = /\\([ \\!"#$%&'()*+,./:;<=>?@[\]^_`{|}~-])/g;
|
||||||
|
function superscript(state, silent) {
|
||||||
|
const max = state.posMax;
|
||||||
|
const start = state.pos;
|
||||||
|
if (state.src.charCodeAt(start) !== 94 /* ^ */) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (silent) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// don't run any pairs in validation mode
|
||||||
|
if (start + 2 >= max) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
state.pos = start + 1;
|
||||||
|
let found = false;
|
||||||
|
while (state.pos < max) {
|
||||||
|
if (state.src.charCodeAt(state.pos) === 94 /* ^ */) {
|
||||||
|
found = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
state.md.inline.skipToken(state);
|
||||||
|
}
|
||||||
|
if (!found || start + 1 === state.pos) {
|
||||||
|
state.pos = start;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const content = state.src.slice(start + 1, state.pos);
|
||||||
|
// don't allow unescaped spaces/newlines inside
|
||||||
|
if (content.match(/(^|[^\\])(\\\\)*\s/)) {
|
||||||
|
state.pos = start;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// found!
|
||||||
|
state.posMax = state.pos;
|
||||||
|
state.pos = start + 1;
|
||||||
|
// Earlier we checked !silent, but this implementation does not need it
|
||||||
|
const token_so = state.push("sup_open", "sup", 1);
|
||||||
|
token_so.markup = "^";
|
||||||
|
const token_t = state.push("text", "", 0);
|
||||||
|
token_t.content = content.replace(UNESCAPE_RE, "$1");
|
||||||
|
const token_sc = state.push("sup_close", "sup", -1);
|
||||||
|
token_sc.markup = "^";
|
||||||
|
state.pos = state.posMax + 1;
|
||||||
|
state.posMax = max;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
function sup_plugin(md) {
|
||||||
|
md.inline.ruler.after("emphasis", "sup", superscript);
|
||||||
|
}
|
||||||
|
return sup_plugin;
|
||||||
|
}));
|
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
@ -15,6 +15,25 @@ RibbonWindow {
|
||||||
minimumHeight: 800
|
minimumHeight: 800
|
||||||
title: qsTr("Protocol Parser") + ` V${PPAPP_Version}`
|
title: qsTr("Protocol Parser") + ` V${PPAPP_Version}`
|
||||||
|
|
||||||
|
title_bar.right_content:RowLayout{
|
||||||
|
spacing: 1
|
||||||
|
layoutDirection: Qt.RightToLeft
|
||||||
|
RibbonButton{
|
||||||
|
show_bg:false
|
||||||
|
icon_source: RibbonIcons.QuestionCircle
|
||||||
|
icon_source_filled: RibbonIcons_Filled.QuestionCircle
|
||||||
|
tip_text: qsTr("帮助")
|
||||||
|
hover_color: Qt.rgba(0,0,0, 0.3)
|
||||||
|
pressed_color: Qt.rgba(0,0,0, 0.4)
|
||||||
|
text_color: title_bar.title_text_color
|
||||||
|
text_color_reverse: false
|
||||||
|
onClicked: {
|
||||||
|
show_popup("components/HelpView.qml")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
TabBar{
|
TabBar{
|
||||||
id: tab_bar
|
id: tab_bar
|
||||||
center_view: center_view
|
center_view: center_view
|
||||||
|
|
|
@ -0,0 +1,73 @@
|
||||||
|
import QtQuick
|
||||||
|
import QtQuick.Controls
|
||||||
|
import QtQuick.Layouts
|
||||||
|
import RibbonUI
|
||||||
|
import ProtocolParser
|
||||||
|
import "."
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id:root
|
||||||
|
implicitHeight: 500
|
||||||
|
implicitWidth: 500
|
||||||
|
property string title: qsTr("帮助")
|
||||||
|
|
||||||
|
RibbonButton{
|
||||||
|
anchors{
|
||||||
|
top:parent.top
|
||||||
|
margins: 4
|
||||||
|
right:parent.right
|
||||||
|
}
|
||||||
|
show_bg: false
|
||||||
|
show_hovered_bg: false
|
||||||
|
icon_source: RibbonIcons.Dismiss
|
||||||
|
onClicked: window_popup.close()
|
||||||
|
}
|
||||||
|
|
||||||
|
RibbonText{
|
||||||
|
anchors{
|
||||||
|
top:parent.top
|
||||||
|
topMargin: 10
|
||||||
|
horizontalCenter: parent.horizontalCenter
|
||||||
|
}
|
||||||
|
text: title
|
||||||
|
}
|
||||||
|
|
||||||
|
RibbonMarkDownViewer{
|
||||||
|
id: viewer
|
||||||
|
anchors.fill: parent
|
||||||
|
text: Tools.readAll(':/qt/qml/ProtocolParser/documents/menu.md')
|
||||||
|
base_url: 'qrc:/qt/qml/ProtocolParser/documents/'
|
||||||
|
}
|
||||||
|
|
||||||
|
RibbonButton{
|
||||||
|
anchors{
|
||||||
|
bottom: root.bottom
|
||||||
|
bottomMargin: 10
|
||||||
|
left: root.left
|
||||||
|
leftMargin: 30
|
||||||
|
}
|
||||||
|
show_bg: false
|
||||||
|
show_hovered_bg: false
|
||||||
|
icon_source: RibbonIcons.ArrowReply
|
||||||
|
text: qsTr('返回目录')
|
||||||
|
onClicked: viewer.go_back()
|
||||||
|
visible: viewer.can_goback
|
||||||
|
show_tooltip: false
|
||||||
|
}
|
||||||
|
|
||||||
|
RibbonButton{
|
||||||
|
anchors{
|
||||||
|
bottom: root.bottom
|
||||||
|
bottomMargin: 10
|
||||||
|
right: root.right
|
||||||
|
rightMargin: 30
|
||||||
|
}
|
||||||
|
show_bg: false
|
||||||
|
show_hovered_bg: false
|
||||||
|
icon_source: RibbonIcons.ArrowForward
|
||||||
|
text: qsTr('返回帮助')
|
||||||
|
onClicked: viewer.go_forward()
|
||||||
|
visible: viewer.can_goforward
|
||||||
|
show_tooltip: false
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,154 @@
|
||||||
|
import QtQuick
|
||||||
|
import RibbonUI
|
||||||
|
import QtWebView
|
||||||
|
import ProtocolParser
|
||||||
|
import "qrc:/qt/qml/ProtocolParser/markdown-it.js" as MarkdownIt
|
||||||
|
import "qrc:/qt/qml/ProtocolParser/markdown-it-deflist.js" as MarkdownItDeflist
|
||||||
|
import "qrc:/qt/qml/ProtocolParser/markdown-it-emoji.js" as MarkdownItEmoji
|
||||||
|
import "qrc:/qt/qml/ProtocolParser/markdown-it-sub.js" as MarkdownItSub
|
||||||
|
import "qrc:/qt/qml/ProtocolParser/markdown-it-sup.js" as MarkdownItSup
|
||||||
|
import "qrc:/qt/qml/ProtocolParser/markdown-it-abbr.js" as MarkdownItAbbr
|
||||||
|
import "qrc:/qt/qml/ProtocolParser/markdown-it-container.js" as MarkdownItContainer
|
||||||
|
import "qrc:/qt/qml/ProtocolParser/markdown-it-footnote.js" as MarkdownItFootnote
|
||||||
|
import "qrc:/qt/qml/ProtocolParser/markdown-it-ins.js" as MarkdownItIns
|
||||||
|
import "qrc:/qt/qml/ProtocolParser/markdown-it-mark.js" as MarkdownItMark
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id:root
|
||||||
|
property string text: ""
|
||||||
|
property string base_url: ""
|
||||||
|
property int page_height: 0
|
||||||
|
property alias can_goback: viewer.canGoBack
|
||||||
|
property alias can_goforward: viewer.canGoForward
|
||||||
|
|
||||||
|
onTextChanged: reload()
|
||||||
|
|
||||||
|
WebView{
|
||||||
|
id: viewer
|
||||||
|
anchors{
|
||||||
|
top: parent.top
|
||||||
|
left: parent.left
|
||||||
|
margins: 30
|
||||||
|
}
|
||||||
|
width: parent.width
|
||||||
|
height: parent.height
|
||||||
|
property int pre_height: 0
|
||||||
|
onLoadingChanged: function(request){
|
||||||
|
if (request.status === WebView.LoadStartedStatus)
|
||||||
|
{
|
||||||
|
if(request.url.toString().match(/^http(s)?:\/\/.+/))
|
||||||
|
{
|
||||||
|
viewer.stop()
|
||||||
|
Qt.openUrlExternally(request.url)
|
||||||
|
}
|
||||||
|
else if (request.url.toString().match(/^qrc?:\/.+/))
|
||||||
|
{
|
||||||
|
console.log(request.url)
|
||||||
|
text = Tools.readAll(request.url.toString().replace("qrc:/",":/"))
|
||||||
|
pre_height = 0
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (request.status === WebView.LoadSucceededStatus)
|
||||||
|
{
|
||||||
|
viewer.width = parent.width - (anchors.margins * 2)
|
||||||
|
viewer.height = parent.height - (anchors.margins * 2)
|
||||||
|
viewer.runJavaScript(`document.body.scrollTop = ${viewer.pre_height};`)
|
||||||
|
get_height()
|
||||||
|
}
|
||||||
|
else if (request.status === WebView.LoadFailedStatus)
|
||||||
|
console.error(request.errorString)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Component.onCompleted: {
|
||||||
|
load()
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections{
|
||||||
|
target: RibbonTheme
|
||||||
|
function onDark_modeChanged(){
|
||||||
|
reload()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function get_height()
|
||||||
|
{
|
||||||
|
viewer.runJavaScript("document.body.scrollHeight", function(height) {
|
||||||
|
page_height = height
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function set_current_height(height)
|
||||||
|
{
|
||||||
|
viewer.runJavaScript(`document.body.scrollTop = ${height};`)
|
||||||
|
viewer.pre_height = height
|
||||||
|
}
|
||||||
|
|
||||||
|
function reload()
|
||||||
|
{
|
||||||
|
viewer.runJavaScript("document.body.scrollTop", function(height) {
|
||||||
|
viewer.pre_height = height
|
||||||
|
load()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function load()
|
||||||
|
{
|
||||||
|
var style = Tools.readAll(":/qt/qml/ProtocolParser/resources/theme.css")
|
||||||
|
var prism = Tools.readAll(`:/qt/qml/ProtocolParser/resources/prism-${RibbonTheme.dark_mode ? 'dark' : 'light'}.css`)
|
||||||
|
var prismjs = Tools.readAll(":/qt/qml/ProtocolParser/prism.js")
|
||||||
|
var ex = `
|
||||||
|
html {
|
||||||
|
height:100%;
|
||||||
|
overflow:hidden;
|
||||||
|
position:relative;
|
||||||
|
}
|
||||||
|
.markdown-body {
|
||||||
|
box-sizing: border-box;
|
||||||
|
min-width: 200px;
|
||||||
|
max-width: 980px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 45px;
|
||||||
|
height:100%;
|
||||||
|
overflow:auto;
|
||||||
|
position:relative;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 767px) {
|
||||||
|
.markdown-body {
|
||||||
|
padding: 15px;
|
||||||
|
}
|
||||||
|
}`
|
||||||
|
var md = markdownit({
|
||||||
|
html: true,
|
||||||
|
linkify: true,
|
||||||
|
typographer: true
|
||||||
|
});
|
||||||
|
md.use(markdownitDeflist)
|
||||||
|
md.use(markdownitEmoji)
|
||||||
|
md.use(markdownitSub)
|
||||||
|
md.use(markdownitSup)
|
||||||
|
md.use(markdownitAbbr)
|
||||||
|
md.use(markdownitContainer)
|
||||||
|
md.use(markdownitFootnote)
|
||||||
|
md.use(markdownitIns)
|
||||||
|
md.use(markdownitMark)
|
||||||
|
var result = md.render(text);
|
||||||
|
result = result.replace(new RegExp("href=\"(./)?", "gi"), 'href="'+base_url);
|
||||||
|
viewer.loadHtml(`<html data-theme=${RibbonTheme.dark_mode ? 'dark' : 'light'}>` + '<head><meta charset="UTF-8", name="viewport" content="width=device-width, initial-scale=1">' +
|
||||||
|
"<style>" + ex + prism + style + "</style></head>" + "<body>" + '<script>' + prismjs + '</script>' +
|
||||||
|
'<article class="markdown-body">' + result + '</article>' + "</body></html>")
|
||||||
|
}
|
||||||
|
|
||||||
|
function go_back()
|
||||||
|
{
|
||||||
|
viewer.goBack()
|
||||||
|
}
|
||||||
|
|
||||||
|
function go_forward()
|
||||||
|
{
|
||||||
|
viewer.goForward()
|
||||||
|
}
|
||||||
|
}
|
|
@ -188,7 +188,28 @@ Item{
|
||||||
: serial_send_type_combo.currentText === "回车" ? SerialPortManager.WithCarriageEnter
|
: serial_send_type_combo.currentText === "回车" ? SerialPortManager.WithCarriageEnter
|
||||||
: serial_send_type_combo.currentText === "换行" ? SerialPortManager.WithLineFeed
|
: serial_send_type_combo.currentText === "换行" ? SerialPortManager.WithLineFeed
|
||||||
: SerialPortManager.WithCarriageEnterAndLineFeed
|
: SerialPortManager.WithCarriageEnterAndLineFeed
|
||||||
SerialPortManager.write(message_sender_textbox.text)
|
let data = input_validate(message_sender_textbox.text)
|
||||||
|
switch(data)
|
||||||
|
{
|
||||||
|
case 'empty':
|
||||||
|
message_sender_textbox.text = "待发送内容为空!"
|
||||||
|
return
|
||||||
|
case 'hex':
|
||||||
|
message_sender_textbox.text = "请严格按照16进制输入!"
|
||||||
|
return
|
||||||
|
case 'case':
|
||||||
|
message_sender_textbox.text = "请使用全大写或全小写16进制输入!"
|
||||||
|
return
|
||||||
|
case 'blank':
|
||||||
|
message_sender_textbox.text = "首尾部不可出现空格!"
|
||||||
|
return
|
||||||
|
case 'separate':
|
||||||
|
message_sender_textbox.text = "字节间需用空格分隔!"
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
break
|
||||||
|
}
|
||||||
|
SerialPortManager.write(data)
|
||||||
serial_view.message_model.append({
|
serial_view.message_model.append({
|
||||||
time: Qt.formatDateTime(new Date(), "yyyy-MM-dd hh:mm:ss.zzz"),
|
time: Qt.formatDateTime(new Date(), "yyyy-MM-dd hh:mm:ss.zzz"),
|
||||||
note_text: message_sender_textbox.text,
|
note_text: message_sender_textbox.text,
|
||||||
|
@ -196,8 +217,27 @@ Item{
|
||||||
})
|
})
|
||||||
message_sender_textbox.textedit.clear()
|
message_sender_textbox.textedit.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
enabled: SerialPortManager.opened
|
enabled: SerialPortManager.opened
|
||||||
|
function input_validate(str)
|
||||||
|
{
|
||||||
|
if (str.length === 0 && serial_send_type_combo.currentText === "无")
|
||||||
|
return 'empty';
|
||||||
|
if (!/^[0-9a-z ]+$/i.test(str) && SerialPortManager.send_hex) {
|
||||||
|
return 'hex';
|
||||||
|
}
|
||||||
|
if ((!(str === str.toUpperCase() || str === str.toLowerCase())) && SerialPortManager.send_hex) {
|
||||||
|
return 'case';
|
||||||
|
}
|
||||||
|
if ((str.startsWith(' ') || str.endsWith(' ')) && SerialPortManager.send_hex) {
|
||||||
|
return 'blank';
|
||||||
|
}
|
||||||
|
for (let i = 0; (i < str.length - 1) && SerialPortManager.send_hex; i += 3) {
|
||||||
|
if (str[i + 2] !== ' ' && i !== str.length - 2) {
|
||||||
|
return 'separate';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return str;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,296 @@
|
||||||
|
html {
|
||||||
|
font-size: 100%;
|
||||||
|
overflow-y: scroll;
|
||||||
|
-webkit-text-size-adjust: 100%;
|
||||||
|
-ms-text-size-adjust: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
color: #444;
|
||||||
|
font-family: Georgia, Palatino, 'Palatino Linotype', Times, 'Times New Roman', serif;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5em;
|
||||||
|
padding: 1em;
|
||||||
|
margin: auto;
|
||||||
|
max-width: 42em;
|
||||||
|
background: #fefefe;
|
||||||
|
}
|
||||||
|
|
||||||
|
a {
|
||||||
|
color: #8aaaeb;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:visited {
|
||||||
|
color: #0645ad;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover {
|
||||||
|
color: #06e;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:active {
|
||||||
|
color: #faa700;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:focus {
|
||||||
|
outline: thin dotted;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:hover,
|
||||||
|
a:active {
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
::-moz-selection {
|
||||||
|
background: rgba(255, 255, 0, 0.3);
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
::selection {
|
||||||
|
background: rgba(255, 255, 0, 0.3);
|
||||||
|
color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
a::-moz-selection {
|
||||||
|
background: rgba(255, 255, 0, 0.3);
|
||||||
|
color: #0645ad;
|
||||||
|
}
|
||||||
|
|
||||||
|
a::selection {
|
||||||
|
background: rgba(255, 255, 0, 0.3);
|
||||||
|
color: #0645ad;
|
||||||
|
}
|
||||||
|
|
||||||
|
p {
|
||||||
|
margin: 1em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1,
|
||||||
|
h2,
|
||||||
|
h3,
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
font-weight: normal;
|
||||||
|
line-height: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4,
|
||||||
|
h5,
|
||||||
|
h6 {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 2.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2 {
|
||||||
|
font-size: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h3 {
|
||||||
|
font-size: 1.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h4 {
|
||||||
|
font-size: 1.2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h5 {
|
||||||
|
font-size: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
h6 {
|
||||||
|
font-size: 0.9em;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
color: #666666;
|
||||||
|
margin: 0;
|
||||||
|
padding-left: 3em;
|
||||||
|
border-left: 0.5em #eee solid;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
display: block;
|
||||||
|
border: 0;
|
||||||
|
border-top: 1px solid #aaa;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
margin: 1em 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre,
|
||||||
|
code,
|
||||||
|
kbd,
|
||||||
|
samp {
|
||||||
|
font-family: monospace, monospace;
|
||||||
|
_font-family: 'courier new', monospace;
|
||||||
|
font-size: 0.98em;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
white-space: pre;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
}
|
||||||
|
|
||||||
|
b,
|
||||||
|
strong {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
dfn {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
|
||||||
|
ins {
|
||||||
|
background: #ff9;
|
||||||
|
color: #000;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
mark {
|
||||||
|
background: #ff0;
|
||||||
|
color: #000;
|
||||||
|
font-style: italic;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub,
|
||||||
|
sup {
|
||||||
|
font-size: 75%;
|
||||||
|
line-height: 0;
|
||||||
|
position: relative;
|
||||||
|
vertical-align: baseline;
|
||||||
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
top: -0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
sub {
|
||||||
|
bottom: -0.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul,
|
||||||
|
ol {
|
||||||
|
margin: 1em 0;
|
||||||
|
padding: 0 0 0 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
li p:last-child {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
dd {
|
||||||
|
margin: 0 0 0 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
border: 0;
|
||||||
|
-ms-interpolation-mode: bicubic;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
border-spacing: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
td {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (min-width: 480px) {
|
||||||
|
body {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media only screen and (min-width: 768px) {
|
||||||
|
body {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media print {
|
||||||
|
* {
|
||||||
|
background: transparent !important;
|
||||||
|
color: black !important;
|
||||||
|
filter: none !important;
|
||||||
|
-ms-filter: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-size: 12pt;
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
a,
|
||||||
|
a:visited {
|
||||||
|
text-decoration: underline;
|
||||||
|
}
|
||||||
|
|
||||||
|
hr {
|
||||||
|
height: 1px;
|
||||||
|
border: 0;
|
||||||
|
border-bottom: 1px solid black;
|
||||||
|
}
|
||||||
|
|
||||||
|
a[href]:after {
|
||||||
|
content: " (" attr(href) ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
abbr[title]:after {
|
||||||
|
content: " (" attr(title) ")";
|
||||||
|
}
|
||||||
|
|
||||||
|
.ir a:after,
|
||||||
|
a[href^="javascript:"]:after,
|
||||||
|
a[href^="#"]:after {
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
pre,
|
||||||
|
blockquote {
|
||||||
|
border: 1px solid #999;
|
||||||
|
padding-right: 1em;
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
tr,
|
||||||
|
img {
|
||||||
|
page-break-inside: avoid;
|
||||||
|
}
|
||||||
|
|
||||||
|
img {
|
||||||
|
max-width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
@page :left {
|
||||||
|
margin: 15mm 20mm 15mm 10mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
@page :right {
|
||||||
|
margin: 15mm 10mm 15mm 20mm;
|
||||||
|
}
|
||||||
|
|
||||||
|
p,
|
||||||
|
h2,
|
||||||
|
h3 {
|
||||||
|
orphans: 3;
|
||||||
|
widows: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
h2,
|
||||||
|
h3 {
|
||||||
|
page-break-after: avoid;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
/* PrismJS 1.29.0
|
||||||
|
https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript+arduino+armasm+bash+c+cpp+docker+git+go+go-module+gradle+http+hpkp+hsts+java+javadoc+javadoclike+jsdoc+js-extras+json+json5+jsonp+jsstacktrace+js-templates+kotlin+latex+log+markup-templating+mermaid+objectivec+perl+php+phpdoc+php-extras+plsql+powerquery+powershell+qml+ruby+rust+shell-session+sql+typescript+wasm+yaml&plugins=line-highlight+line-numbers+highlight-keywords */
|
||||||
|
code[class*=language-],pre[class*=language-]{color:#ccc;background:0 0;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#161b22}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.block-comment,.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#999}.token.punctuation{color:#ccc}.token.attr-name,.token.deleted,.token.namespace,.token.tag{color:#e2777a}.token.function-name{color:#6196cc}.token.boolean,.token.function,.token.number{color:#f08d49}.token.class-name,.token.constant,.token.property,.token.symbol{color:#f8c555}.token.atrule,.token.builtin,.token.important,.token.keyword,.token.selector{color:#cc99cd}.token.attr-value,.token.char,.token.regex,.token.string,.token.variable{color:#7ec699}.token.entity,.token.operator,.token.url{color:#67cdcc}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}.token.inserted{color:green}
|
||||||
|
pre[data-line]{position:relative;padding:1em 0 1em 3em}.line-highlight{position:absolute;left:0;right:0;padding:inherit 0;margin-top:1em;background:hsla(24,20%,50%,.08);background:linear-gradient(to right,hsla(24,20%,50%,.1) 70%,hsla(24,20%,50%,0));pointer-events:none;line-height:inherit;white-space:pre}@media print{.line-highlight{-webkit-print-color-adjust:exact;color-adjust:exact}}.line-highlight:before,.line-highlight[data-end]:after{content:attr(data-start);position:absolute;top:.4em;left:.6em;min-width:1em;padding:0 .5em;background-color:hsla(24,20%,50%,.4);color:#f4f1ef;font:bold 65%/1.5 sans-serif;text-align:center;vertical-align:.3em;border-radius:999px;text-shadow:none;box-shadow:0 1px #fff}.line-highlight[data-end]:after{content:attr(data-end);top:auto;bottom:.4em}.line-numbers .line-highlight:after,.line-numbers .line-highlight:before{content:none}pre[id].linkable-line-numbers span.line-numbers-rows{pointer-events:all}pre[id].linkable-line-numbers span.line-numbers-rows>span:before{cursor:pointer}pre[id].linkable-line-numbers span.line-numbers-rows>span:hover:before{background-color:rgba(128,128,128,.2)}
|
||||||
|
pre[class*=language-].line-numbers{position:relative;padding-left:3.8em;counter-reset:linenumber}pre[class*=language-].line-numbers>code{position:relative;white-space:inherit}.line-numbers .line-numbers-rows{position:absolute;pointer-events:none;top:0;font-size:100%;left:-3.8em;width:3em;letter-spacing:-1px;border-right:1px solid #999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.line-numbers-rows>span{display:block;counter-increment:linenumber}.line-numbers-rows>span:before{content:counter(linenumber);color:#999;display:block;padding-right:.8em;text-align:right}
|
|
@ -0,0 +1,5 @@
|
||||||
|
/* PrismJS 1.29.0
|
||||||
|
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript+arduino+armasm+bash+c+cpp+docker+git+go+go-module+gradle+http+hpkp+hsts+java+javadoc+javadoclike+jsdoc+js-extras+json+json5+jsonp+jsstacktrace+js-templates+kotlin+latex+log+markup-templating+mermaid+objectivec+perl+php+phpdoc+php-extras+plsql+powerquery+powershell+qml+ruby+rust+shell-session+sql+typescript+wasm+yaml&plugins=line-highlight+line-numbers+highlight-keywords */
|
||||||
|
code[class*=language-],pre[class*=language-]{color:#000;background:0 0;text-shadow:0 1px #fff;font-family:Consolas,Monaco,'Andale Mono','Ubuntu Mono',monospace;font-size:1em;text-align:left;white-space:pre;word-spacing:normal;word-break:normal;word-wrap:normal;line-height:1.5;-moz-tab-size:4;-o-tab-size:4;tab-size:4;-webkit-hyphens:none;-moz-hyphens:none;-ms-hyphens:none;hyphens:none}code[class*=language-] ::-moz-selection,code[class*=language-]::-moz-selection,pre[class*=language-] ::-moz-selection,pre[class*=language-]::-moz-selection{text-shadow:none;background:#b3d4fc}code[class*=language-] ::selection,code[class*=language-]::selection,pre[class*=language-] ::selection,pre[class*=language-]::selection{text-shadow:none;background:#b3d4fc}@media print{code[class*=language-],pre[class*=language-]{text-shadow:none}}pre[class*=language-]{padding:1em;margin:.5em 0;overflow:auto}:not(pre)>code[class*=language-],pre[class*=language-]{background:#f6f8fa}:not(pre)>code[class*=language-]{padding:.1em;border-radius:.3em;white-space:normal}.token.cdata,.token.comment,.token.doctype,.token.prolog{color:#708090}.token.punctuation{color:#999}.token.namespace{opacity:.7}.token.boolean,.token.constant,.token.deleted,.token.number,.token.property,.token.symbol,.token.tag{color:#905}.token.attr-name,.token.builtin,.token.char,.token.inserted,.token.selector,.token.string{color:#690}.language-css .token.string,.style .token.string,.token.entity,.token.operator,.token.url{color:#9a6e3a;background:hsla(0,0%,100%,.5)}.token.atrule,.token.attr-value,.token.keyword{color:#07a}.token.class-name,.token.function{color:#dd4a68}.token.important,.token.regex,.token.variable{color:#e90}.token.bold,.token.important{font-weight:700}.token.italic{font-style:italic}.token.entity{cursor:help}
|
||||||
|
pre[data-line]{position:relative;padding:1em 0 1em 3em}.line-highlight{position:absolute;left:0;right:0;padding:inherit 0;margin-top:1em;background:hsla(24,20%,50%,.08);background:linear-gradient(to right,hsla(24,20%,50%,.1) 70%,hsla(24,20%,50%,0));pointer-events:none;line-height:inherit;white-space:pre}@media print{.line-highlight{-webkit-print-color-adjust:exact;color-adjust:exact}}.line-highlight:before,.line-highlight[data-end]:after{content:attr(data-start);position:absolute;top:.4em;left:.6em;min-width:1em;padding:0 .5em;background-color:hsla(24,20%,50%,.4);color:#f4f1ef;font:bold 65%/1.5 sans-serif;text-align:center;vertical-align:.3em;border-radius:999px;text-shadow:none;box-shadow:0 1px #fff}.line-highlight[data-end]:after{content:attr(data-end);top:auto;bottom:.4em}.line-numbers .line-highlight:after,.line-numbers .line-highlight:before{content:none}pre[id].linkable-line-numbers span.line-numbers-rows{pointer-events:all}pre[id].linkable-line-numbers span.line-numbers-rows>span:before{cursor:pointer}pre[id].linkable-line-numbers span.line-numbers-rows>span:hover:before{background-color:rgba(128,128,128,.2)}
|
||||||
|
pre[class*=language-].line-numbers{position:relative;padding-left:3.8em;counter-reset:linenumber}pre[class*=language-].line-numbers>code{position:relative;white-space:inherit}.line-numbers .line-numbers-rows{position:absolute;pointer-events:none;top:0;font-size:100%;left:-3.8em;width:3em;letter-spacing:-1px;border-right:1px solid #999;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none}.line-numbers-rows>span{display:block;counter-increment:linenumber}.line-numbers-rows>span:before{content:counter(linenumber);color:#999;display:block;padding-right:.8em;text-align:right}
|
File diff suppressed because it is too large
Load Diff
|
@ -5,6 +5,7 @@
|
||||||
#include <FramelessHelper/Core/private/framelessconfig_p.h>
|
#include <FramelessHelper/Core/private/framelessconfig_p.h>
|
||||||
#include <QTranslator>
|
#include <QTranslator>
|
||||||
#include <QQmlContext>
|
#include <QQmlContext>
|
||||||
|
#include <QtWebView>
|
||||||
|
|
||||||
FRAMELESSHELPER_USE_NAMESPACE
|
FRAMELESSHELPER_USE_NAMESPACE
|
||||||
#ifdef RIBBONUI_BUILD_STATIC_LIB
|
#ifdef RIBBONUI_BUILD_STATIC_LIB
|
||||||
|
@ -13,6 +14,7 @@ Q_IMPORT_QML_PLUGIN(RibbonUIPlugin)
|
||||||
int main(int argc, char *argv[])
|
int main(int argc, char *argv[])
|
||||||
{
|
{
|
||||||
qputenv("QT_QUICK_CONTROLS_STYLE","Basic");
|
qputenv("QT_QUICK_CONTROLS_STYLE","Basic");
|
||||||
|
QtWebView::initialize();
|
||||||
FramelessHelper::Quick::initialize();
|
FramelessHelper::Quick::initialize();
|
||||||
QGuiApplication app(argc, argv);
|
QGuiApplication app(argc, argv);
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
#include "tools.h"
|
||||||
|
#include <QMutex>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QFileInfo>
|
||||||
|
|
||||||
|
Tools::Tools(QObject *parent)
|
||||||
|
: QObject{parent}
|
||||||
|
{}
|
||||||
|
|
||||||
|
Tools* Tools::instance(){
|
||||||
|
static QMutex mutex;
|
||||||
|
QMutexLocker locker(&mutex);
|
||||||
|
|
||||||
|
static Tools *singleton = nullptr;
|
||||||
|
if (!singleton) {
|
||||||
|
singleton = new Tools();
|
||||||
|
}
|
||||||
|
return singleton;
|
||||||
|
}
|
||||||
|
|
||||||
|
void Tools::setDefaultStyleSheet(QQuickTextDocument *qd, QString path)
|
||||||
|
{
|
||||||
|
auto td = qd->textDocument();
|
||||||
|
td->setDefaultStyleSheet(readAll(path));
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Tools::readAll(QString path)
|
||||||
|
{
|
||||||
|
QFile file(path);
|
||||||
|
if(!file.exists()){
|
||||||
|
qWarning()<<"File not exist!";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
if(!file.open(QIODevice::ReadOnly|QIODevice::Text)){
|
||||||
|
qWarning()<<"Cannot open the file!";
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
auto content = file.readAll();
|
||||||
|
return QString(content);
|
||||||
|
}
|
||||||
|
|
||||||
|
void Tools::setBaseUrl(QQuickTextDocument *qd, QUrl url)
|
||||||
|
{
|
||||||
|
qd->textDocument()->setBaseUrl(url);
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Tools::fileSuffix(const QString &filePath)
|
||||||
|
{
|
||||||
|
QFileInfo info(filePath);
|
||||||
|
return info.suffix();
|
||||||
|
}
|
||||||
|
|
||||||
|
QString Tools::fileDir(const QString &filePath)
|
||||||
|
{
|
||||||
|
QFileInfo info(filePath);
|
||||||
|
return info.absolutePath();
|
||||||
|
}
|
|
@ -0,0 +1,19 @@
|
||||||
|
## 目录
|
||||||
|
## 1. [协议解析](protocol.md)
|
||||||
|
+ 概述
|
||||||
|
+ 密钥
|
||||||
|
+ 认证流程
|
||||||
|
+ 数据结构
|
||||||
|
+ 接口解析
|
||||||
|
## 2. [串口使用](serialport.md)
|
||||||
|
+ 参数配置
|
||||||
|
+ 串口消息设置
|
||||||
|
+ 串口助手设置
|
||||||
|
+ 串口数据流向
|
||||||
|
## 3. [紫蜂协议](zigbee.md)
|
||||||
|
+ 设备列表
|
||||||
|
+ 密钥管理
|
||||||
|
+ 调试选项
|
||||||
|
## 4. [其他](others.md)
|
||||||
|
+ 主题
|
||||||
|
+ 数据结构自定义
|
|
@ -0,0 +1,16 @@
|
||||||
|
## 1. 主题
|
||||||
|
+ 选择应用程序主题(浅色/深色/跟随系统)。注意,在`Windows`系统下,手动设置日间/夜间主题是无效的,仅建议使用**跟随系统**。
|
||||||
|
## 2. 数据结构自定义
|
||||||
|
+ 此选项可以自定义下位机节点发送的**数据帧**中携带的数据结构组成。
|
||||||
|
+ 受限于`DL-LN3X`模块最大传输长度,仅建议携带最大不超过`16`字节的数据。
|
||||||
|
+ 如需自定义数据结构,需从默认数据结构中删除一项或多项,再根据实际情况输入数据名称和选择数据类型。
|
||||||
|
+ 请注意,数据按照**小端序**排列。
|
||||||
|
+ 下位机如使用结构体组织传感器数据,请务必注意结构体**内存对齐**的问题。
|
||||||
|
+ 默认数据结构如下:
|
||||||
|
|
||||||
|
| 数据 | 数据类型(字节数) | 说明 |
|
||||||
|
|:----:|:----:|:----:|
|
||||||
|
| 温度 | `float, 4 Bytes` | 位于第`0`~`3`字节 |
|
||||||
|
| 湿度 | `float, 4 Bytes` | 位于第`4`~`7`字节 |
|
||||||
|
| 气体浓度 | `float, 4 Bytes` | 位于第`8`~`11`字节 |
|
||||||
|
| 火焰指数 | `float, 4 Bytes` | 位于第`12`~`15`字节 |
|
Binary file not shown.
Before Width: | Height: | Size: 962 KiB After Width: | Height: | Size: 975 KiB |
Binary file not shown.
Before Width: | Height: | Size: 835 KiB After Width: | Height: | Size: 841 KiB |
|
@ -0,0 +1,236 @@
|
||||||
|
## 1. 概述
|
||||||
|
本协议是一套运行在应用层的,用于端到端通讯的轻量级安全协议,支持非对称/对称的双向、单向认证及加密通讯。该协议的认证和加密通讯由SM系列(SM2、SM3、SM4)算法保障,安全性较强,可抵御重放攻击。协议易拓展,数据包采用二进制传输,适合在多种窄带宽场景下使用(目前已在嵌入式、ARM、X86/64平台上测试通过)。
|
||||||
|
|
||||||
|
## 2. 密钥
|
||||||
|
本协议采用两类密钥:一是用于双向认证的SM2公私钥对,二是用于对称双、单向认证和加密传输的SM4密钥。
|
||||||
|
+ SM2密钥长度为:公钥64Bytes(512Bits),私钥32 Bytes(256Bits)。密文长度为明文长度再加上96Bytes。签名长度为64Bytes(被签名数据内包含随机数盐值,因此对同一数据签名的结果不相同)。
|
||||||
|
+ SM4密钥长度为:16Bytes(128Bits)。
|
||||||
|
|
||||||
|
## 3. 认证流程
|
||||||
|
### 3.1 非对称双向认证
|
||||||
|
由于SM2算法会占用一部分资源,非对称双向认证一般用于性能较好的终端(如性能较强的单片机、树莓派或者PC等设备)之间的认证,性能较差的单片机将会出现死机等预料外情况(板载硬件密码算法模块除外)。
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant 服务器
|
||||||
|
participant 客户端
|
||||||
|
客户端-->服务器: 客户端验证服务器
|
||||||
|
客户端->>服务器: 包含‘Hello’信息的ssl_frame请求
|
||||||
|
服务器->>客户端: 包含‘Hello’和64Bytes服务器公钥信息的ssl_frame响应
|
||||||
|
客户端-->>客户端: 用服务器公钥加密自己的公钥
|
||||||
|
客户端->>服务器: 包含服务器公钥加密的客户端公钥信息的ssl_frame数据包
|
||||||
|
服务器-->>服务器: 用自己的私钥解密出客户端公钥
|
||||||
|
服务器->>客户端: 包含用客户端公钥加密的‘Verified’信息的ssl_frame数据包
|
||||||
|
客户端-->>客户端: 用自己的私钥解密出‘Verified’信息
|
||||||
|
客户端->>服务器: 包含服务器公钥加密的‘OK’信息的ssl_frame数据包
|
||||||
|
服务器-->客户端: 服务器验证客户端
|
||||||
|
服务器-->>服务器: 生成8Bytes随机数挑战值
|
||||||
|
服务器->>客户端: 包含客户端公钥加密的8Bytes挑战值信息的ssl_frame数据包
|
||||||
|
客户端-->>客户端: 用自己的私钥解密出挑战值,<br>并用自己的私钥对它签名
|
||||||
|
客户端->>服务器: 包含客户端公钥(64Bytes)、签名(64Bytes)、<br>挑战值(8Bytes)共136Bytes数据的ssl_frame数据包
|
||||||
|
服务器-->>服务器: 进行三轮比对:<br>1.将获得的客户端公钥与<br>第一轮验证中获取的客户端公钥进行比对<br>2.将获得的挑战值与发送的进行比对<br>3.用客户端公钥对挑战值签名数据进行验签
|
||||||
|
服务器->>客户端: 包含客户端公钥加密的‘OK’信息的ssl_frame数据包
|
||||||
|
服务器-->客户端: 双向认证结束
|
||||||
|
```
|
||||||
|
### 3.2 对称双向认证
|
||||||
|
对称双向认证使用基于SM3的HMAC算法实现。通过验证双方是否共同持有同一对预设定密钥来实现认证,在实际运用过程中,为节省资源,服务器发送给客户端的‘Identified’信息可不加密,采用明文传输(因为后续传输的数据包均为加密数据包)。
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant 服务器
|
||||||
|
participant 客户端
|
||||||
|
客户端-->>客户端: 生成1Byte随机数,并对其使用基于SM3的HMAC算法和<br>预设定16Bytes密钥生成值
|
||||||
|
客户端->>服务器: 包含随机数和使用预设定16Bytes密钥生成的<br>HMAC值信息的hmac_frame请求
|
||||||
|
服务器-->>服务器: 使用自己拥有的预设定16Bytes密钥对收到的<br>1Byte随机数进行HMAC运算,并将值与收到值进行比对
|
||||||
|
服务器->>客户端: 包含使用预设定16Bytes密钥加密的<br>‘Identified’信息的crypto_zdata_frame数据包
|
||||||
|
客户端-->>客户端: 用预设定16Bytes密钥解密出‘Identified’信息
|
||||||
|
客户端->>服务器: 包含正常数据的用预设定密钥加密的<br>crypto_zdata_frame数据包
|
||||||
|
```
|
||||||
|
|
||||||
|
## 4. 数据结构
|
||||||
|
### 4.1 基础帧(base_frame)
|
||||||
|
基础帧是本协议传输最底层的帧,用于承载其他帧。
|
||||||
|
| 帧结构 | 长度(Bytes) | 说明 |
|
||||||
|
|:----:|:----:|:----:|
|
||||||
|
| head | 2 | 包头,数值固定为`0xAAAD` |
|
||||||
|
| ori_addr | 2 | 源地址,即该帧发送方的地址 |
|
||||||
|
| des_addr | 2 | 目的地址,即该帧接收方的地址 |
|
||||||
|
| node_addr | 2 | 节点地址,仅在此帧被服务器进行路由时使用,<br>用以标记该帧由哪个节点最先产生 |
|
||||||
|
| id | 2 | 帧标号,用以与`rand_num`共同唯一标记一个帧,防止重放攻击 |
|
||||||
|
| length | 2 | 帧长度(包括帧头),最大为65535个Bits |
|
||||||
|
| reset_num | 1 | 重置标记位,用以告知对方在接收此帧后,<br>下一帧的帧标号将归零,`rand_num`将被重新生成 |
|
||||||
|
| rand_num | 1 | 随机数 |
|
||||||
|
| data | 可变长度 | 数据最大不超过(8191 - `BASE_FRAME_PREFIX_LEN`)= 8177 |
|
||||||
|
|
||||||
|
另有其他说明如下:
|
||||||
|
+ 宏定义`BASE_FRAME_PREFIX_LEN`:基础帧帧头长度,固定为14Bytes
|
||||||
|
+ 宏定义`BASE_FRAME_HEAD`:基础帧帧头,固定为`0xAAAD`
|
||||||
|
+ 宏定义`BASE_FRAME_RESET_NUM`:基础帧重置阈值:当发送帧数达到该阈值时,下一帧将被重置帧标号和随机数,该阈值一般为`10000`,最大不超过`65535`
|
||||||
|
+ 宏定义`new_base_frame(num)`:用以生成基础帧数据结构,`num`为`data`部分长度,使用时请注意强制类型转换
|
||||||
|
### 4.2 数据帧(data_frame)
|
||||||
|
数据帧是本协议传输中最顶层的帧,用于承载各类数据。
|
||||||
|
| 帧结构 | 长度(Bytes) | 说明 |
|
||||||
|
|:----:|:----:|:----:|
|
||||||
|
| head | 2 | 包头,数值固定为`0xAAAA` |
|
||||||
|
| type | 1 | 数据类型,由用户根据业务不同自定义 |
|
||||||
|
| use_crc | 1 | CRC标记位,当其为`0xFF`时将对数据进行CRC校验 |
|
||||||
|
| data_length | 2 | 数据长度,不包括包头 |
|
||||||
|
| crc | 2 | CRC16校验码,当CRC标记位使能且对数据进行CRC校验后数值与该码不同,包将被判定为损坏 |
|
||||||
|
| data | 可变长度 | 根据底层帧的不同,数据最大不超过(8191 - `BASE_FRAME_PREFIX_LEN` - `DATA_FRAME_PREFIX_LEN`)= 8169,若叠加多重帧,需要减去对应帧的帧头 |
|
||||||
|
|
||||||
|
另有其他说明如下:
|
||||||
|
+ 宏定义`DATA_FRAME_PREFIX_LEN`:数据帧帧头长度,固定为8Bytes
|
||||||
|
+ 宏定义`DATA_FRAME_HEAD`:数据帧帧头,固定为`0xAAAA`
|
||||||
|
+ 宏定义`new_data_frame(num)`:用以生成数据帧数据结构,`num`为`data`部分长度,使用时请注意强制类型转换
|
||||||
|
### 4.3 数字信封(digi_env)
|
||||||
|
数字信封是通信双方进行非对称双向认证后,用于加密传输数据的数据结构。
|
||||||
|
| 帧结构 | 长度(Bytes) | 说明 |
|
||||||
|
|:----:|:----:|:----:|
|
||||||
|
| head | 2 | 包头,数值固定为`0xAAAB` |
|
||||||
|
| length | 2 | 长度,包括包头 |
|
||||||
|
| crypted_session_key | 112 | 由接收方公钥加密的16Bytes会话密钥再加上96Bytes的额外数据 |
|
||||||
|
| data | 可变长度 | 由会话密钥进行SM4加密的数据长度,数据最大不超过(8191 - `BASE_FRAME_PREFIX_LEN` - `DIGI_ENV_PREFIX_LEN` - `DIGI_ENV_SESSION_KEY_LEN`)= 8061,又因SM4的密文为16的倍数,因此最大不能超过8048 |
|
||||||
|
|
||||||
|
另有其他说明如下:
|
||||||
|
+ 宏定义`SM4_PADDING_LEN`:SM4默认填充长度,固定为16Bytes
|
||||||
|
+ 宏定义`DIGI_ENV_PREFIX_LEN`:数字信封包头长度,固定为4Bytes
|
||||||
|
+ 宏定义`DIGI_ENV_SESSION_KEY_LEN`:数字信封密态会话密钥长度,固定为112Bytes
|
||||||
|
+ 宏定义`DIGI_ENV_HEAD`:数字信封包头,固定为`0xAAAB`
|
||||||
|
+ 宏定义`new_digi_env(num)`:用以生成数字信封数据结构,`num`为`data`部分长度,使用时请注意强制类型转换
|
||||||
|
### 4.4 非对称双向认证包(ssl_frame)
|
||||||
|
非对称双向认证包负责承载非对称双向认证相关数据。
|
||||||
|
| 帧结构 | 长度(Bytes) | 说明 |
|
||||||
|
|:----:|:----:|:----:|
|
||||||
|
| head | 2 | 包头,数值固定为`0xAAAC` |
|
||||||
|
| length | 2 | 长度,包括包头 |
|
||||||
|
| data | 可变长度 | 数据最大不超过(8191 - `BASE_FRAME_PREFIX_LEN` - `SSL_FRAME_PREFIX_LEN`)= 8173 |
|
||||||
|
|
||||||
|
另有其他说明如下:
|
||||||
|
+ 宏定义`SSL_FRAME_PREFIX_LEN`:非对称双向认证包包头长度,固定为4Bytes
|
||||||
|
+ 宏定义`SSL_FRAME_HEAD`:非对称双向认证包包头,固定为`0xAAAC`
|
||||||
|
+ 宏定义`new_ssl_frame(num)`:用以生成非对称双向认证包数据结构,`num`为`data`部分长度,使用时请注意强制类型转换
|
||||||
|
### 4.5 对称双向认证包(hmac_frame)
|
||||||
|
对称双向认证包负责承载对称双向认证相关数据。
|
||||||
|
| 帧结构 | 长度(Bytes) | 说明 |
|
||||||
|
|:----:|:----:|:----:|
|
||||||
|
| head | 2 | 包头,数值固定为`0xAAAE` |
|
||||||
|
| length | 2 | 长度,包括包头 |
|
||||||
|
| value | 1 | 随机数 |
|
||||||
|
| hmac | 32 | 随机数`value`对应的HMAC数值 |
|
||||||
|
|
||||||
|
另有其他说明如下:
|
||||||
|
+ 宏定义`HMAC_FRAME_PREFIX_LEN`:对称双向认证包包头长度,固定为4Bytes
|
||||||
|
+ 宏定义`HMAC_FRAME_HEAD`:非对称双向认证包包头,固定为`0xAAAE`
|
||||||
|
### 4.6 对称加密数据帧(crypto_zdata_frame)
|
||||||
|
对称加密数据帧用于通信双方完成对称双向认证后数据加密传输。
|
||||||
|
| 帧结构 | 长度(Bytes) | 说明 |
|
||||||
|
|:----:|:----:|:----:|
|
||||||
|
| head | 2 | 包头,数值固定为`0xAAAF` |
|
||||||
|
| length | 2 | 长度,包括包头 |
|
||||||
|
| crc | 2 | CRC16校验码,对解密数据进行CRC校验后数值与该码不同,将被判定为解密失败 |
|
||||||
|
| data | 可变长度 | 数据最大不超过(8191 - `BASE_FRAME_PREFIX_LEN` - `CRYPTO_ZDATA_FRAME_PREFIX_LEN`)= 8171 |
|
||||||
|
|
||||||
|
另有其他说明如下:
|
||||||
|
+ 宏定义`CRYPTO_ZDATA_FRAME_PREFIX_LEN`:对称加密数据帧帧头长度,固定为6Bytes
|
||||||
|
+ 宏定义`CRYPTO_ZDATA_FRAME_HEAD`:对称加密数据帧帧头,固定为`0xAAAF`
|
||||||
|
### 4.7 设备信息存储(device)
|
||||||
|
协议采用`device`结构体用作存储相关通讯数据。
|
||||||
|
| 结构 | 说明 |
|
||||||
|
|:----:|:----:|
|
||||||
|
| addr | 用以保存通讯对象的地址(可以是发送方,也可以是接收方) |
|
||||||
|
| id | 缓存发送/接收到的基础帧帧标号 |
|
||||||
|
| rand_num | 缓存发送/接收到的基础帧随机数 |
|
||||||
|
| verified | 标记该通讯对象是否通过认证 |
|
||||||
|
| online | 标记该通讯对象是否在线 |
|
||||||
|
| logined | 仅作为服务器时使用,记录该通讯对象是否已经登录 |
|
||||||
|
| stage | 仅在非对称双向认证时使用,记录该通讯对象正处于认证的第几阶段 |
|
||||||
|
| chlg_buf | 仅在非对称双向认证时使用,记录该通讯对象的挑战值 |
|
||||||
|
| ip | 仅在TCP链路中使用,记录该通讯对象的IP地址 |
|
||||||
|
| port | 仅在TCP链路中使用,记录该通讯对象的端口号 |
|
||||||
|
| key_pair | 仅在非对称双向认证时使用,记录该通讯对象的公钥和会话密钥 |
|
||||||
|
|
||||||
|
|
||||||
|
## 5. 接口解析
|
||||||
|
### 5.1 协议封装/解析
|
||||||
|
#### 5.1.1 void protocal_wrapper(data_frame *frame, u8 type, u16 length, u8 *data, bool use_crc)
|
||||||
|
将对应数据打包进数据帧中。
|
||||||
|
| 变量名 | 输入/输出 | 说明 |
|
||||||
|
|:----:|:----:|:----:|
|
||||||
|
| frame | 输出 | 数据帧指针 |
|
||||||
|
| type | 输入 | 数据类型 |
|
||||||
|
| length | 输入 | 数据长度 |
|
||||||
|
| use_crc | 输入 | 是否启用CRC验证 |
|
||||||
|
#### 5.1.2 void base_frame_maker(void *in_frame, base_frame *out_frame, u16 dest_addr,device *dev,u16 node_addr=0)
|
||||||
|
将对应数据打包进基础帧中。
|
||||||
|
| 变量名 | 输入/输出 | 说明 |
|
||||||
|
|:----:|:----:|:----:|
|
||||||
|
| in_frame | 输入 | 数据包指针,指向需要被打包进基础帧的数据地址 |
|
||||||
|
| out_frame | 输出 | 基础帧指针 |
|
||||||
|
| dest_addr | 输入 | 目的地址 |
|
||||||
|
| dev | 输出 | 发送方的`device`数据结构指针,缓存帧标号和随机数等相关数据 |
|
||||||
|
| node_addr | 输入 | 仅在服务器端路由时使用,用以保存该基础帧的最初创建者地址 |
|
||||||
|
#### 5.1.3 bool base_frame_parser(base_frame *in_frame, void **out_frame, device *dev)
|
||||||
|
从基础帧中解析出数据,返回`true`为解析成功,`false`为解析失败,需要检查包基础帧数据内容是否错误。
|
||||||
|
| 变量名 | 输入/输出 | 说明 |
|
||||||
|
|:----:|:----:|:----:|
|
||||||
|
| in_frame | 输入 | 基础帧指针 |
|
||||||
|
| out_frame | 输出 | 数据包指针,指向需要从基础帧中解析出的数据包地址 |
|
||||||
|
| dev | 输出 | 接收方的`device`数据结构指针,缓存帧标号和随机数等相关数据 |
|
||||||
|
#### 5.1.4 void ssl_frame_maker(ssl_frame *frame, u8 *data, int data_len)
|
||||||
|
将相关数据打包进非对称双向认证包中。
|
||||||
|
| 变量名 | 输入/输出 | 说明 |
|
||||||
|
|:----:|:----:|:----:|
|
||||||
|
| in_frame | 输出 | 非对称双向认证包指针,指向需要被打包进非对称双向认证包的数据地址 |
|
||||||
|
| data | 输入 | 数据指针 |
|
||||||
|
| data_len | 输入 | 数据长度 |
|
||||||
|
#### 5.1.5 void zigbee_data_encrypt(data_frame *data, crypto_zdata_frame *zdata, bool (* SM4_encrypt)(u8 *key_origin, u32 key_len, u8 *in_origin, u32 in_len, u8 *out, u32 *out_len, bool use_real_cbc),QString en_key = "")
|
||||||
|
对数据进行对称加密。
|
||||||
|
| 变量名 | 输入/输出 | 说明 |
|
||||||
|
|:----:|:----:|:----:|
|
||||||
|
| data | 输入 | 待加密的数据指针 |
|
||||||
|
| zdata | 输出 | 待填充的对称加密数据包指针 |
|
||||||
|
| SM4_encrypt | 输入 | SM4加密算法函数指针 |
|
||||||
|
| en_key | 输入 | 若不提供密钥,则将使用默认密钥进行加密 |
|
||||||
|
#### 5.1.6 bool zigbee_data_dectypt(data_frame *data, crypto_zdata_frame *zdata,bool (* SM4_decrypt)(u8 *key_origin, u32 key_len, u8 *in, u32 in_len, u8 *out, u32 *out_len, bool use_real_cbc),QString en_key = "")
|
||||||
|
对数据进行对称解密,返回`true`代表解密成功,`false`代表解密失败,需要检查输入是否错误。
|
||||||
|
| 变量名 | 输入/输出 | 说明 |
|
||||||
|
|:----:|:----:|:----:|
|
||||||
|
| data | 输出 | 待解密的数据地址指针 |
|
||||||
|
| zdata | 输入 | 收到的对称加密数据包指针 |
|
||||||
|
| SM4_decrypt | 输入 | SM4解密算法函数指针 |
|
||||||
|
| en_key | 输入 | 若不提供密钥,则将使用默认密钥进行解密 |
|
||||||
|
### 5.2 协议认证/验证
|
||||||
|
#### 5.2.1 void HMAC_identify(device *self, device *node, hmac_frame *hframe, void (*sendTonode)(ZigbeeFrame &data), void (*SM3_HMAC)(u8 *key, int keylen,u8 *input, int ilen,u8 output[32]))
|
||||||
|
对接收到的HMAC包进行数据认证。
|
||||||
|
| 变量名 | 输入/输出 | 说明 |
|
||||||
|
|:----:|:----:|:----:|
|
||||||
|
| self | 输出 | 接收方的`device`数据结构指针,缓存帧标号和随机数等相关数据 |
|
||||||
|
| node | 输出 | 发送方的`device`数据结构指针,缓存帧标号和随机数等相关数据 |
|
||||||
|
| hframe | 输入 | 接收到的对称双向认证包指针 |
|
||||||
|
| sendTonode | 输入 | 发送数据函数的指针 |
|
||||||
|
#### 5.2.2 bool data_frame_verify(data_frame *frame)
|
||||||
|
对数据帧进行验证,返回`true`代表包有效,`false`代表包损坏。
|
||||||
|
| 变量名 | 输入/输出 | 说明 |
|
||||||
|
|:----:|:----:|:----:|
|
||||||
|
| frame | 输入 | 待检验的数据帧指针 |
|
||||||
|
#### 5.2.3 void HMAC_changeVerifykey(u8 key[16], device* self, device *node, void (*sendTonode)(ZigbeeFrame &data),bool (* SM4_encrypt)(u8 *key_origin, u32 key_len, u8 *in_origin, u32 in_len, u8 *out, u32 *out_len, bool use_real_cbc))
|
||||||
|
发送对称双向认证密钥更换指令包。
|
||||||
|
| 变量名 | 输入/输出 | 说明 |
|
||||||
|
|:----:|:----:|:----:|
|
||||||
|
| key | 输入 | 新密钥 |
|
||||||
|
| self | 输出 | 接收方的`device`数据结构指针,缓存帧标号和随机数等相关数据 |
|
||||||
|
| node | 输出 | 发送方的`device`数据结构指针,缓存帧标号和随机数等相关数据 |
|
||||||
|
| sendTonode | 输入 | 发送数据函数的指针 |
|
||||||
|
| SM4_encrypt | 输入 | SM4加密算法函数指针 |
|
||||||
|
### 5.3 工具
|
||||||
|
#### 5.3.1 uint16_t crc16_xmodem(const uint8_t *buffer, uint32_t buffer_length)
|
||||||
|
生成CRC16校验码。
|
||||||
|
| 变量名 | 输入/输出 | 说明 |
|
||||||
|
|:----:|:----:|:----:|
|
||||||
|
| buffer | 输入 | 需要生成校验码的数据指针 |
|
||||||
|
| buffer_length | 输入 | 需要生成校验码的数据长度 |
|
||||||
|
#### 5.3.2 bool bytecmp(u8 *a, u8 *b, u16 length)
|
||||||
|
将两个输入进行逐字比较,即`memcmp`,返回`true`代表完全一致,`false`代表存在差异。
|
||||||
|
| 变量名 | 输入/输出 | 说明 |
|
||||||
|
|:----:|:----:|:----:|
|
||||||
|
| a | 输入 | 需对比数据A |
|
||||||
|
| b | 输入 | 需对比数据B |
|
||||||
|
| length | 输入 | 需对比数据长度 |
|
|
@ -0,0 +1,25 @@
|
||||||
|
## 1. 参数配置
|
||||||
|
| 参数 | 说明 |
|
||||||
|
|:----:|:----:|
|
||||||
|
| 端口号 | 选择`DL-LN3X`模块对应的端口号 |
|
||||||
|
| 校验码 | 选择串口校验码(如奇校验/偶校验),一般为无 |
|
||||||
|
| 波特率 | 选择串口波特率,`DL-LN3X`模块默认为`115200` |
|
||||||
|
| 数据位 | 选择一个数据帧内包含多少位数据,一般默认为`8`位,即一个字节 |
|
||||||
|
| 停止位 | 选择停止位长度,一般默认为`1`位 |
|
||||||
|
|
||||||
|
## 2. 串口消息设置
|
||||||
|
| 选项 | 说明 |
|
||||||
|
|:----:|:----:|
|
||||||
|
| 显示消息解析 | 为`ZigBee`信息流气泡中的消息内容显示解析 |
|
||||||
|
| 自动定位最新消息 | `ZigBee`信息流自动跟踪最新收到的消息 |
|
||||||
|
| 清除旧消息 | 清空`ZigBee`信息流 |
|
||||||
|
|
||||||
|
## 3. 串口助手设置
|
||||||
|
| 选项 | 说明 |
|
||||||
|
|:----:|:----:|
|
||||||
|
| 十六进制接收 | 让串口助手以`HEX`的形式显示收到的数据 |
|
||||||
|
| 十六进制发送 | 让串口助手以`HEX`的形式发送用户输入的数据 |
|
||||||
|
|
||||||
|
## 4. 串口数据流向
|
||||||
|
标记当前串口数据显示在哪个界面,如紫蜂/串口助手。
|
||||||
|
将鼠标放在窗口右侧边缘,点击出现的按钮即可进入串口助手界面。
|
|
@ -0,0 +1,26 @@
|
||||||
|
## 1. 设备列表
|
||||||
|
+ 在此选择需要管理的设备列表。
|
||||||
|
+ 仅当节点被从**等待队列**中加入**认证列表**后,上位机才会下发**验证通过**指令。
|
||||||
|
+ 被加入黑名单的设备,上位机将**不会**响应其任何请求,直到被移除出黑名单。
|
||||||
|
|
||||||
|
| 选项 | 说明 |
|
||||||
|
|:----:|:----:|
|
||||||
|
| 等待队列 | 管理正在等待验证的节点列表 |
|
||||||
|
| 认证列表 | 管理已经通过认证的节点列表 |
|
||||||
|
| 黑名单列表 | 管理上位机忽略请求的设备列表 |
|
||||||
|
|
||||||
|
## 2. 密钥管理
|
||||||
|
+ 在此处可以更改当前使用的`SM4`加解密和`HMAC`所使用的`16`字节密钥。
|
||||||
|
+ 请务必输入:`16`字节`HEX`形式密钥,即至少为`32`个字符。
|
||||||
|
+ 密钥每个字节间可以用空格分隔。
|
||||||
|
+ 密钥可以全大写,也可以全小写,但不可以混用。
|
||||||
|
|
||||||
|
## 3. 调试选项
|
||||||
|
+ 此处的设置为开发者调试软件所用,建议普通用户不要使用。
|
||||||
|
|
||||||
|
| 选项 | 说明 |
|
||||||
|
|:----:|:----:|
|
||||||
|
| 生成接收数据 | 在**接收数据流**中模拟生成一条接收数据 |
|
||||||
|
| 生成验证数据 | 在**认证信息流**中模拟生成一条验证数据 |
|
||||||
|
| 生成更换密钥数据 | 在**认证信息流**中模拟生成一条更换密钥数据,仅在存在历史密钥的情况下可用 |
|
||||||
|
| 事件历史信息 | 查看**事件总线**的调试数据 |
|
Loading…
Reference in New Issue