移动端更新

This commit is contained in:
molong committed 2026-02-21 14:47:54 +08:00
1 parent 7f8e68bb0a
commit c0d27be99b
432 files changed
+100843

No files matched your search

+2
View File
@@ -0,0 +1,2 @@
# 插件
本目录下是一些扩展插件,可以按照需要选用以实现更加丰富的功能
+25
View File
@@ -0,0 +1,25 @@
# audio
功能:音乐播放器
大小:*≈4KB*
支持平台:
| 微信小程序 | QQ 小程序 | 百度小程序 | 支付宝小程序 | 头条小程序 | uni-app |
|:---:|:---:|:---:|:---:|:---:|:---:|
| √ | √ | √ | √ | √ | √(nvue 不支持) |
百度小程序原生包在此 [问题](https://smartprogram.baidu.com/forum/topic/show/125787) 未解决前无法使用
说明:
在大多数小程序平台,*audio* 标签已被废弃或无法使用,本插件可以代替 *audio* 标签播放音乐,并实现以下优化:
1. *pause-video* 属性也可以应用于音频,即播放一个音视频时可以自动暂停其他正在播放的音视频
2. 增加了一个可以拖动的进度条
3. 组件大小可以根据页面宽度自动调整
4. 支持 *autoplay* 属性
5. 播放被后台打断时,页面显示后自动继续播放
基础库要求:
支付宝 *1.23.4+* ,其余平台满足最低要求即可
第 *5* 条仅微信 *2.2.3+* 、*QQ*、百度支持
?> 如果希望页面上使用本组件,组件的路径为 *path/to/mp-html/audio/audio*
属性和事件基本同 *audio* 组件,组件实例上提供了 *setSrc*、*play*、*seek*、*pause*、*stop* 方法可供控制播放状态
+11
View File
@@ -0,0 +1,11 @@
module.exports = {
usingComponents: {
'my-audio': '../audio/audio'
},
handler (file) {
// 删去原来的 audio 标签
if (file.basename === 'node.wxml' || file.basename === 'node.vue') {
file.contents = Buffer.from(file.contents.toString().replace(/<audio[\s\S]+?>/, ''))
}
}
}
+7
View File
@@ -0,0 +1,7 @@
const ctx = {}
module.exports = {
get: id => ctx[id],
set: (id, vm) => { ctx[id] = vm },
remove: id => { ctx[id] = undefined }
}
+34
View File
@@ -0,0 +1,34 @@
/**
* @fileoverview audio 插件
*/
const context = require('./context')
let index = 0
function Audio (vm) {
this.vm = vm
}
Audio.prototype.onUpdate = function () {
this.audios = []
}
Audio.prototype.onParse = function (node) {
if (node.name === 'audio') {
if (!node.attrs.id) {
node.attrs.id = 'a' + index++
}
this.audios.push(node.attrs.id)
}
}
Audio.prototype.onLoad = function () {
setTimeout(() => {
for (let i = 0; i < this.audios.length; i++) {
const ctx = context.get(this.audios[i])
ctx.id = this.audios[i]
this.vm._videos.push(ctx)
}
}, 500)
}
module.exports = Audio
@@ -0,0 +1,189 @@
/**
* @fileoverview audio 组件
*/
const context = require('./context')
Component({
data: {
time: '00:00'
},
properties: {
name: String, // 音乐名
author: String, // 作者
poster: String, // 海报图片地址
autoplay: Boolean, // 是否自动播放
controls: Boolean, // 是否显示控件
loop: Boolean, // 是否循环播放
src: { // 源地址
type: String,
observer (src) {
this.setSrc(src)
}
}
},
created () {
// 创建内部 context
this._ctx = wx.createInnerAudioContext()
this._ctx.onError(err => {
this.setData({
error: true
})
this.triggerEvent('error', err)
})
this._ctx.onTimeUpdate(() => {
const time = this._ctx.currentTime
const min = parseInt(time / 60)
const sec = Math.ceil(time % 60)
const data = {}
data.time = (min > 9 ? min : '0' + min) + ':' + (sec > 9 ? sec : '0' + sec)
// 不在拖动状态下需要更新进度条
if (!this.lastTime) {
data.value = time / this._ctx.duration * 100
}
this.setData(data)
})
this._ctx.onEnded(() => {
if (!this.properties.loop) {
this.setData({
playing: false
})
}
})
// #ifndef ALIPAY
},
attached () {
context.set(this.id, this)
// #endif
// #ifdef MP-ALIPAY
context.set(this.properties.id, this)
this.setSrc(this.properties.src)
// #endif
},
// #ifdef MP-ALIPAY
didUpdate (e) {
if (e.src !== this.properties.src) {
this.setSrc(this.properties.src)
}
},
// #endif
detached () {
this._ctx.destroy()
// #ifndef MP-ALIPAY
context.remove(this.id)
// #endif
// #ifdef MP_ALIPAY
context.remove(this.properties.id)
// #endif
},
// #ifndef ALIPAY | TOUTIAO
pageLifetimes: {
show () {
// 播放被后台打断时,页面显示后自动继续播放
if (this.data.playing && this._ctx.paused) {
this._ctx.play()
}
}
},
// #endif
methods: {
/**
* @description 设置源
* @param {string} src 源地址
*/
setSrc (src) {
this._ctx.autoplay = this.properties.autoplay
this._ctx.loop = this.properties.loop
this._ctx.src = src
if (this.properties.autoplay && !this.data.playing) {
this.setData({
playing: true
})
}
},
/**
* @description 播放音乐
*/
play () {
this._ctx.play()
this.setData({
playing: true
})
this.triggerEvent('play'
// #ifdef MP-ALIPAY
, {
target: {
id: this.props.id
}
}
// #endif
)
},
/**
* @description 暂停音乐
*/
pause () {
this._ctx.pause()
this.setData({
playing: false
})
this.triggerEvent('pause')
},
/**
* @description 设置播放速率
* @param {Number} rate 播放速率
*/
playbackRate (rate) {
this._ctx.playbackRate = rate
},
/**
* @description 停止音乐
*/
stop () {
this._ctx.stop()
this.setData({
playing: false,
time: '00:00'
})
this.triggerEvent('stop')
},
/**
* @description 控制进度
* @param {number} sec 秒数
*/
seek (sec) {
this._ctx.seek(sec)
},
/**
* @description 移动进度条
* @param {event} e
* @private
*/
_seeking (e) {
// 避免过于频繁 setData
if (e.timeStamp - this.lastTime < 200) return
const time = Math.round(e.detail.value / 100 * this._ctx.duration)
const min = parseInt(time / 60)
const sec = time % 60
this.setData({
time: (min > 9 ? min : '0' + min) + ':' + (sec > 9 ? sec : '0' + sec)
})
this.lastTime = e.timeStamp
},
/**
* @description 进度条移动完毕
* @param {event} e
* @private
*/
_seeked (e) {
this._ctx.seek(e.detail.value / 100 * this._ctx.duration)
this.lastTime = undefined
}
}
})
@@ -0,0 +1,3 @@
{
"component": true
}
@@ -0,0 +1,17 @@
<view wx:if="{{controls}}" class="_contain">
<!-- 海报和按钮 -->
<view class="_poster" style="background-image:url('{{poster}}')">
<view class="_button" bindtap="{{playing?'pause':'play'}}">
<view class="{{playing?'_pause':'_play'}}" />
</view>
</view>
<!-- 曲名和作者 -->
<view class="_title">
<view class="_name">{{name||'未知音频'}}</view>
<view class="_author">{{author||'未知作者'}}</view>
</view>
<!-- 进度条 -->
<slider class="_slider" activeColor="#585959" block-size="12" disabled="{{error}}" value="{{value}}" bindchanging="_seeking" bindchange="_seeked" />
<!--播放时间-->
<view class="_time">{{time}}</view>
</view>
@@ -0,0 +1,127 @@
/* 顶层容器 */
._contain {
position: relative;
display: inline-flex;
width: 290px;
background-color: #fcfcfc;
border: 1px solid #e0e0e0;
border-radius: 2px;
}
/* 播放、暂停按钮 */
._button {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
overflow: hidden;
background-color: rgb(0, 0, 0, 0.2);
border: 1px solid white;
border-radius: 50%;
opacity: 0.9;
}
._play {
margin-left: 2px;
border-top: 4px solid transparent;
border-bottom: 4px solid transparent;
border-left: 8px solid white;
}
._pause {
width: 8px;
height: 8px;
background-color: white;
}
/* 海报 */
._poster {
display: flex;
align-items: center;
justify-content: center;
width: 70px;
height: 70px;
background-color: #e6e6e6;
background-size: contain;
}
/* 标题栏 */
._title {
flex: 1;
margin: 4px 0 0 14px;
text-align: left;
}
._author {
width: 45px;
font-size: 12px;
color: #888;
}
._name {
width: 140px;
font-size: 15px;
line-height: 39px;
}
._author,
._name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 进度条 */
._slider {
position: absolute;
right: 16px;
bottom: 8px;
width: 140px;
margin: 0;
}
/* 播放时间 */
._time {
margin: 7px 14px 0 0;
font-size: 12px;
color: #888;
}
/* 响应式布局,大屏幕用更大的尺寸 */
@media (min-width: 400px) {
._contain {
width: 380px;
}
._button {
width: 26px;
height: 26px;
}
._poster {
width: 90px;
height: 90px;
}
._author {
width: 60px;
font-size: 15px;
}
._name {
width: 180px;
font-size: 19px;
line-height: 55px;
}
._slider {
right: 20px;
bottom: 10px;
width: 180px;
}
._time {
font-size: 15px;
}
}
@@ -0,0 +1,3 @@
module.exports = {
template: '<my-audio wx:if="{{n.name==\'audio\'}}" id="{{n.attrs.id}}" class="{{n.attrs.class}}" style="{{n.attrs.style}}" author="{{n.attrs.author}}" controls="{{n.attrs.controls}}" autoplay="{{n.attrs.autoplay}}" loop="{{n.attrs.loop}}" name="{{n.attrs.name}}" poster="{{n.attrs.poster}}" src="{{n.src[ctrl[i]||0]}}" data-i="{{i}}" data-source="audio" bindplay="play" binderror="mediaError" />'
}
+269
View File
@@ -0,0 +1,269 @@
<template>
<view v-if="controls" @click="onClick" class="_contain">
<!-- 海报和按钮 -->
<view class="_poster" :style="'background-image:url('+poster+')'">
<view class="_button" @tap="_buttonTap">
<view :class="playing?'_pause':'_play'" />
</view>
</view>
<!-- 曲名和作者 -->
<view class="_title">
<view class="_name">{{name||'未知音频'}}</view>
<view class="_author">{{author||'未知作者'}}</view>
</view>
<!-- 进度条 -->
<slider class="_slider" activeColor="#585959" block-size="12" handle-size="12" :disabled="error" :value="value" @changing="_seeking" @change="_seeked" />
<!--播放时间-->
<view class="_time">{{time||'00:00'}}</view>
</view>
</template>
<script>
/**
* @fileoverview audio 组件
*/
import context from './context'
export default {
data () {
return {
error: false,
playing: false,
time: '00:00',
value: 0
}
},
props: {
aid: String,
name: String, // 音乐名
author: String, // 作者
poster: String, // 海报图片地址
autoplay: [Boolean, String], // 是否自动播放
controls: [Boolean, String], // 是否显示控件
loop: [Boolean, String], // 是否循环播放
src: String // 源地址
},
watch: {
src (src) {
this.setSrc(src)
}
},
mounted () {
this._ctx = uni.createInnerAudioContext()
this._ctx.onError((err) => {
this.error = true
this.$emit('error', err)
})
this._ctx.onTimeUpdate(() => {
const time = this._ctx.currentTime
const min = parseInt(time / 60)
const sec = Math.ceil(time % 60)
this.time = (min > 9 ? min : '0' + min) + ':' + (sec > 9 ? sec : '0' + sec)
if (!this.lastTime) {
this.value = time / this._ctx.duration * 100 // 不在拖动状态下
}
})
this._ctx.onEnded(() => {
if (!this.loop) {
this.playing = false
}
})
context.set(this.aid, this)
this.setSrc(this.src)
},
beforeDestroy () {
this._ctx.destroy()
context.remove(this.aid)
},
onPageShow () {
if (this.playing && this._ctx.paused) {
this._ctx.play()
}
},
methods: {
// 设置源
setSrc (src) {
this._ctx.autoplay = this.autoplay
this._ctx.loop = this.loop
this._ctx.src = src
if (this.autoplay && !this.playing) {
this.playing = true
}
},
// 播放
play () {
this._ctx.play()
this.playing = true
this.$emit('play', {
target: {
id: this.aid
}
})
},
// 暂停
pause () {
this._ctx.pause()
this.playing = false
this.$emit('pause')
},
// 设置播放速率
playbackRate (rate) {
this._ctx.playbackRate = rate
},
// 移动进度条
seek (sec) {
this._ctx.seek(sec)
},
// 内部方法
_buttonTap () {
if (this.playing) this.pause()
else this.play()
},
_seeking (e) {
// 避免过于频繁 setData
if (e.timeStamp - this.lastTime < 200) return
const time = Math.round(e.detail.value / 100 * this._ctx.duration)
const min = parseInt(time / 60)
const sec = time % 60
this.time = (min > 9 ? min : '0' + min) + ':' + (sec > 9 ? sec : '0' + sec)
this.lastTime = e.timeStamp
},
_seeked (e) {
this.seek(e.detail.value / 100 * this._ctx.duration)
this.lastTime = undefined
},
onClick(e) {
this.$emit('onClick', e)
}
}
}
</script>
<style>
/* 顶层容器 */
._contain {
position: relative;
display: inline-flex;
width: 290px;
background-color: #fcfcfc;
border: 1px solid #e0e0e0;
border-radius: 2px;
}
/* 播放、暂停按钮 */
._button {
display: flex;
align-items: center;
justify-content: center;
width: 20px;
height: 20px;
overflow: hidden;
background-color: rgb(0, 0, 0, 0.2);
border: 1px solid white;
border-radius: 50%;
opacity: 0.9;
}
._play {
margin-left: 2px;
border-top: 4px solid transparent;
border-bottom: 4px solid transparent;
border-left: 8px solid white;
}
._pause {
width: 8px;
height: 8px;
background-color: white;
}
/* 海报 */
._poster {
display: flex;
align-items: center;
justify-content: center;
width: 70px;
height: 70px;
background-color: #e6e6e6;
background-size: contain;
}
/* 标题栏 */
._title {
flex: 1;
margin: 4px 0 0 14px;
text-align: left;
}
._author {
width: 45px;
font-size: 12px;
color: #888;
}
._name {
width: 140px;
font-size: 15px;
line-height: 39px;
}
._author,
._name {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
/* 进度条 */
._slider {
position: absolute;
right: 16px;
bottom: 8px;
width: 140px;
margin: 0;
}
/* 播放时间 */
._time {
margin: 7px 14px 0 0;
font-size: 12px;
color: #888;
}
/* 响应式布局,大屏幕用更大的尺寸 */
@media (min-width: 400px) {
._contain {
width: 380px;
}
._button {
width: 26px;
height: 26px;
}
._poster {
width: 90px;
height: 90px;
}
._author {
width: 60px;
font-size: 15px;
}
._name {
width: 180px;
font-size: 19px;
line-height: 55px;
}
._slider {
right: 20px;
bottom: 10px;
width: 180px;
}
._time {
font-size: 15px;
}
}
</style>
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
template: '<my-audio v-if="n.name==\'audio\'" :class="n.attrs.class" :style="n.attrs.style" :aid="n.attrs.id" :author="n.attrs.author" :controls="n.attrs.controls" :autoplay="n.attrs.autoplay" :loop="n.attrs.loop" :name="n.attrs.name" :poster="n.attrs.poster" :src="n.src[ctrl[i]||0]" :data-i="i" data-source="audio" @play="play" @error="mediaError" />'
}
+30
View File
@@ -0,0 +1,30 @@
# card
功能:商品(联络人)信息卡
大小:*≈7KB*
支持平台:
| 微信小程序 | QQ 小程序 | 百度小程序 | 支付宝小程序 | 头条小程序 | uni-app |
|:---:|:---:|:---:|:---:|:---:|:---:|
| √ | √ | √ | √ | √ | √(nvue 不支持) |
### 效果图
![效果图](../../docs/assets/plugin/card.png)
### 参数列表
|参数名|是否必须|类型|说明|
|:---- |:---|:----- |----- |
|src|是|String|图片Url|
|title|是|String|标题|
|desc|是|String|描述|
|url|是|String|跳转url|
|color|是|String|文字颜色|
|bgcolor|是|String|卡片背景颜色|
|border|是|String|卡片边框颜色|
### 说明:
1. 可以显示商品信息卡片/联络人信息卡片
### 基础库要求:
满足最低要求即可
?> 如果希望页面上使用本组件,组件的路径为 *path/to/mp-html/card/card*
+14
View File
@@ -0,0 +1,14 @@
module.exports = {
usingComponents: {
'my-card': '../card/card'
},
handler (file) {
if (file.isBuffer()) {
let content = file.contents.toString()
if (file.path.includes('parser.js')) {
content = content.replace(/trustTags\s*:\s*makeMap\('/, "trustTags: makeMap('card,").replace(/voidTags\s*:\s*makeMap\('/, "voidTags: makeMap('card,")
}
file.contents = Buffer.from(content)
}
}
}
+7
View File
@@ -0,0 +1,7 @@
/**
* @fileoverview Card 插件
*/
function Card (vm) {
}
module.exports = Card
@@ -0,0 +1,3 @@
module.exports = {
template: '<my-card wx:if="{{n.name==\'card\'}}" class="{{n.attrs.class}}" style="{{n.attrs.style}}" mode="{{opts[5]}}" src="{{n.attrs.src}}" title="{{n.attrs.title}}" desc="{{n.attrs.desc}}" url="{{n.attrs.url}}" color="{{n.attrs.color}}" bgcolor="{{n.attrs.bgcolor}}" border="{{n.attrs.border}}" name="{{n.attrs.name}}" data-i="{{i}}" data-source="card" />'
}
+26
View File
@@ -0,0 +1,26 @@
/**
* @fileoverview card 组件
*/
Component({
properties: {
mode: {
type: Boolean,
default: false
},
src: String,
title: String,
desc: String,
url: String,
color: String,
bgcolor: String,
border: String
},
data: {},
methods: {
onClick (e) {
if (this.properties.url && this.properties.url.trim().length > 6 && !this.properties.mode) {
wx.navigateTo({ url: this.properties.url })
}
}
}
})
@@ -0,0 +1,3 @@
{
"component": true
}
@@ -0,0 +1,9 @@
<view class="card" bindtap="onClick" style="background-color:{{bgcolor||'#a4d0ff'}};border:{{border||'1px solid #FFF'}};color:{{color||'#000'}}">
<image class="card-img" mode="aspectFill" src="{{src}}" />
<view class="text-wrap text-wrap-width" wx:if="{{!!desc}}">
<view class="title one-t">{{title}}</view>
<view class="desc one-t">{{desc}}</view>
</view>
<view wx:else class="text-wrap-width title more-t">{{title}}</view>
<image class="card-icon" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAABCFBMVEUAAAC/v7+qqqqZmZmLi6KJnZ2ImZmHlpaGlKGMjJmSkp6Li5eQkJuKlZ+Pj5mOjpeJkpuNjZ6IkJmHj5eLi5uKkpmGjZqJj5uLi5eIjpmIjZiKj5qKj5mJjpiHjJqJjZaGj5iKj5eIjJmKjpqHi5eGjZeIi5eHjZiIi5eHjJiIjpaHjJeIjZiGjJeIjZiGjJaGjJeIi5aGjJeHi5eHi5aHjJeHjJaHjJeGi5eHjJaHjJeGi5aGi5aHjJeHjJaGjJeGjJeHjJaHi5eGi5eHjJaHjJeGjJaGjJeHi5aHjJeHjJeHi5aGjJeHi5aGjJaHi5eGjJaHi5eHjJaGi5eGi5aHjJeGi5aGi5apAvjmAAAAV3RSTlMABAYKCw0PERMUFRYXGBkbHB0eICEjJiksLS8wMjQ1ODk7PD9ATFZXWFlaW1xdXl+Hi6msu7/Dx8vMzs/R0tTV19na3N3f4uTn6evs7e7v8PHy9PX7/P18cCTXAAABEklEQVRo3u2YWU5CQRQFn4qCM4LzhIoDAorzrIgCigiCimf/O/Gj3UIlmJxaQFXSea/T90aRMcYYY4zpG0ZPu9cZMnAi6SsLBjqS9LnJBcqSpC53Sjs/kqSPNaxwGAqtFbrQXKILjQW68DpPF17m6EI9TRdqM3TheZouVCbpQnkcK5RC4T5BF27jdOFqhC5cDtOFixhdOB+iC2cDdOEoggttLrDbk6QW5/+WJB1T/r1e+FAHWT/2q/35scsiF/w3cdZ/R13Y+8H/MMb6Hycgfz74n6ZYfzXJ+mspyF8I/vos68cep0X4eV2EB4SD4H9bZP3vy+yTtL3KjrGddXgQ34BXCVvwMmT7P69zjDHGGGP6gF83lHISOctsKQAAAABJRU5ErkJggg=="></image>
</view>
@@ -0,0 +1,55 @@
.one-t {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
transition: all linear 0.2s;
}
.more-t {
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
transition: all linear 0.2s;
}
.card {
width: 80%;
margin: 10rpx auto;
max-width: 700rpx;
max-height: 140rpx;
box-sizing: border-box;
overflow: hidden;
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0 20rpx 10rpx;
border-radius: 12rpx;
}
.card-img {
width: 96rpx;
height: 96rpx;
border-radius: 12rpx;
flex: 0 0 96rpx;
}
.card-icon {
width: 30rpx;
height: 96rpx;
}
.card .text-wrap {
display: flex;
flex-direction: column;
justify-content: space-between;
}
.card .text-wrap-width {
width: 72%;
}
.card .title {
font-weight: bold;
font-size: 34rpx;
line-height: 48rpx;
}
.card .desc {
font-size: 27rpx;
line-height: 37rpx;
}
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
template: '<my-card v-if="n.name==\'card\'" :class="n.attrs.class" :style="n.attrs.style" :mode="opts[5]" :src="n.attrs.src" :title="n.attrs.title" :desc="n.attrs.desc" :url="n.attrs.url" :color="n.attrs.color" :bgcolor="n.attrs.bgcolor" :border="n.attrs.border" :name="n.attrs.name" :data-i="i" data-source="card" />'
}
+122
View File
@@ -0,0 +1,122 @@
<template>
<view class="card" @click="onClick" :style="[customStyle]" :data-i="$attrs['data-i']">
<image class="card-img" mode="aspectFill" :src="src" />
<view class="text-wrap text-wrap-width" v-if="!!desc">
<view class="title one-t">{{title}}</view>
<view class="desc one-t">{{desc}}</view>
</view>
<view v-else class="text-wrap-width title more-t">{{title}}</view>
<image class="card-icon" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGAAAABgCAMAAADVRocKAAABCFBMVEUAAAC/v7+qqqqZmZmLi6KJnZ2ImZmHlpaGlKGMjJmSkp6Li5eQkJuKlZ+Pj5mOjpeJkpuNjZ6IkJmHj5eLi5uKkpmGjZqJj5uLi5eIjpmIjZiKj5qKj5mJjpiHjJqJjZaGj5iKj5eIjJmKjpqHi5eGjZeIi5eHjZiIi5eHjJiIjpaHjJeIjZiGjJeIjZiGjJaGjJeIi5aGjJeHi5eHi5aHjJeHjJaHjJeGi5eHjJaHjJeGi5aGi5aHjJeHjJaGjJeGjJeHjJaHi5eGi5eHjJaHjJeGjJaGjJeHi5aHjJeHjJeHi5aGjJeHi5aGjJaHi5eGjJaHi5eHjJaGi5eGi5aHjJeGi5aGi5apAvjmAAAAV3RSTlMABAYKCw0PERMUFRYXGBkbHB0eICEjJiksLS8wMjQ1ODk7PD9ATFZXWFlaW1xdXl+Hi6msu7/Dx8vMzs/R0tTV19na3N3f4uTn6evs7e7v8PHy9PX7/P18cCTXAAABEklEQVRo3u2YWU5CQRQFn4qCM4LzhIoDAorzrIgCigiCimf/O/Gj3UIlmJxaQFXSea/T90aRMcYYY4zpG0ZPu9cZMnAi6SsLBjqS9LnJBcqSpC53Sjs/kqSPNaxwGAqtFbrQXKILjQW68DpPF17m6EI9TRdqM3TheZouVCbpQnkcK5RC4T5BF27jdOFqhC5cDtOFixhdOB+iC2cDdOEoggttLrDbk6QW5/+WJB1T/r1e+FAHWT/2q/35scsiF/w3cdZ/R13Y+8H/MMb6Hycgfz74n6ZYfzXJ+mspyF8I/vos68cep0X4eV2EB4SD4H9bZP3vy+yTtL3KjrGddXgQ34BXCVvwMmT7P69zjDHGGGP6gF83lHISOctsKQAAAABJRU5ErkJggg=="></image>
</view>
</template>
<script>
export default {
props: {
mode: {
type: Boolean,
default: false
},
src: String,
title: String,
desc: String,
url: String,
color: String,
bgcolor: String,
border: String
},
data () {
return {
}
},
computed: {
customStyle () {
return {
'background-color': this.bgColor || '#a4d0ff',
border: this.border || '1px solid #FFF',
color: this.color || '#000'
}
}
},
methods: {
onClick (e) {
if (this.url && this.url.trim().length > 6 && !this.mode) {
uni.navigateTo({ url: this.url })
}
this.$emit('click', e)
}
}
}
</script>
<style lang="scss">
.one-t {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
transition: all linear 0.2s;
}
.more-t {
overflow: hidden;
text-overflow: ellipsis;
word-break:break-all;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
transition: all linear 0.2s;
}
.card {
width: 80%;
margin: 10rpx auto;
max-width: 700rpx;
max-height: 140rpx;
box-sizing: border-box;
overflow: hidden;
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0 20rpx 10rpx;
border-radius: 12rpx;
&-img {
width: 96rpx;
height: 96rpx;
border-radius: 12rpx;
flex: 0 0 96rpx;
}
&-icon {
width: 30rpx;
height: 96rpx;
}
.text-wrap {
display: flex;
flex-direction: column;
justify-content: space-between;
&-width {
width: 72%;
}
}
.title {
font-weight: bold;
font-size: 34rpx;
line-height: 48rpx;
}
.desc {
font-size: 27rpx;
line-height: 37rpx;
}
}
</style>
+137
View File
@@ -0,0 +1,137 @@
# editable
功能:富文本编辑
下表列出了本插件与原生 *editor* 组件的功能差异,可按需选用
| 组件 | 优点 | 缺点 |
|:---:|:---:|:---:|
| 原生 *editor* | 底层通过 *contenteditable* 实现,编辑流畅 | 支持标签少(不支持音视频、表格以及 *section* 等常用标签)、部分小程序平台不支持或低版本不兼容 |
| 本插件 | 支持标签全面、支持平台全面 | 编辑灵活性不够强 |
大小:*≈17.5KB*
支持平台:
| 微信小程序 | QQ 小程序 | 百度小程序 | 支付宝小程序 | 头条小程序 | uni-app |
|:---:|:---:|:---:|:---:|:---:|:---:|
| √ | √ | √ | √ | √ | √(nvue 不支持) |
说明:
引入本插件后,会给组件添加以下属性:
| 属性名 | 类型 | 默认值 | 说明 |
|:---:|:---:|:---:|:---:|
| editable | Boolean | false | 是否开启内容编辑 |
| placeholder | String | 请输入 | 输入框为空时占位符(`2.1.0+`) |
添加以下事件:
| 事件名 | 触发时机 | 用途 |
|:---:|:---:|:---:|
| remove(`2.2.0+`) | 删除图片/视频/音频标签时 | 删除已上传的线上文件 |
支持以下操作:
| 类型 | 操作 |
|:---:|:---:|
| 文本 | 修改 |
| 图片 | 更换链接、调整宽度、设置成超链接(`2.0.4+`)、设置预览图链接、禁用预览、删除 |
| 链接 | 更换链接、删除 |
| 音视频 | 设置封面、设置循环播放、设置自动播放(`2.2.0+`)、删除 |
| 普通标签 | 设置字体大小、斜体、粗体、下划线(`2.0.4+`)、居中、缩进、删除 |
> `2.2.1` 版本起所有标签支持上下移动操作,但仅限同级标签间移动,即在有同级标签且非第一个(或最后一个)时可以上移(或下移)
> 在支付宝小程序中使用时需要在页面样式中添加 *page { position: relative; }* 避免 *tooltip* 错位
> 菜单项可以通过编辑 *plugins/editable/config.js* 进行修改,仅可以删减或调整顺序,添加或更名无效
组件实例上提供了以下方法(*editable* 属性为 *true* 时才可以调用):
| 名称 | 功能 |
|:---:|:---:|
| undo | 撤销一个操作 |
| redo | 重做一个操作 |
| insertHtml | 在光标处插入指定 html 内容(`2.1.0+`) |
| insertImg | 在光标处插入一张图片 |
| insertTable(rows, cols) | 在光标处插入一个 rows 行 cols 列的表格(`2.1.3+`) |
| insertVideo | 在光标处插入一个视频 |
| insertAudio | 在光标处插入一个音频 |
| insertLink | 在光标处插入一个链接 |
| insertText | 在光标处插入一段文本 |
| clear | 清空内容 |
| getContent | 获取编辑后的 html 内容 |
> 考虑到不同场景下希望获取链接的方法不同,需要在初始时给组件设置一个 *getSrc* 方法(否则插入图片、音视频、链接或修改链接等操作无法使用),每次组件内需要链接时会调用此方法,开发者可在此方法中自行决定如何获取链接,返回 **线上地址** 即可(具体用法见下方示例)
编辑完成后,通过 *getContent* 方法获取编辑后的 *html*,最后将 *editable* 属性设置为 *false* 即可正常渲染
> 点击保存按钮时,部分平台 *tap* 事件早于 *blur* 事件触发,直接获取内容可能导致无法获取当前编辑的文本内容,因此建议设置一个小的延时后获取(可参考下方示例,[详细](https://github.com/jin-yufeng/mp-html/issues/368))
示例:
```javascript
Page({
onLoad () {
// ctx 为组件实例,获取方法见上
/**
* @description 设置获取链接的方法
* @param {String} type 链接的类型(img/video/audio/link)
* @param {String} value 修改链接时,这里会传入旧值
* @returns {Promise} 返回线上地址(2.2.0 版本起设置了 domain 属性时,可以缺省主域名)
* type 为 audio/video 时,可以返回一个源地址数组
* 2.1.3 版本起 type 为 audio 时,可以返回一个 object,包含 src、name、author、poster 等字段
* 2.2.0 版本起 type 为 img 时,可以返回一个源地址数组,表示插入多张图片(修改链接时仅限一张)
*/
this.ctx.getSrc = (type, value) => {
return new Promise((resolve, reject) => {
// 以图片为例
if (type == 'img') {
wx.chooseImage({
count: value === undefined ? 9 : 1, // 2.2.0 版本起插入图片时支持多张(修改图片链接时仅限一张)
success: res => {
wx.showLoading({
title: '上传中'
});
(async ()=>{
const arr = []
for (let item of res.tempFilePaths) {
// 依次上传
const src = await upload(item)
arr.push(src)
}
return arr
})().then(res => {
wx.hideLoading()
resolve(res)
})
},
fail: reject
})
}
})
}
},
finishEdit () {
setTimeout(() => {
var html = ctx.getContent() // 获取编辑好的 html
// 上传 html
wx.request({
url: 'xxx',
data: {
html
},
success: () => {
this.setData({
editable: false // 结束编辑
})
}
})
}, 50)
}
})
```
**示例项目**:
微信小程序点击 [代码片段](https://developers.weixin.qq.com/s/GFbJKum77eBy) 即可在微信开发者工具中导入;*uni-app* 下载 [示例项目](https://mp-html.oss-cn-hangzhou.aliyuncs.com/editable.zip) 在 *HBuilder X* 中打开即可体验;注意示例项目中不一定包含最新版本,仅供参考使用方法
注意事项:
1. 不要在 *editable* 属性被设置为 *true* 前通过 *setContent* 方法(用 *content* 属性)设置内容,否则在切换为 *true* 后会变成空白
2. *editable* 属性为 *true* 时不支持在 *scroll-view* 中使用,否则提示框的位置可能不正确
+15
View File
@@ -0,0 +1,15 @@
// 以下项目可以删减或更换顺序,但不能添加或更改名字
module.exports = {
// 普通标签的菜单项
node: ['大小', '颜色', '斜体', '粗体', '下划线', '居中', '缩进', '上移', '下移', '删除'],
// 可以设置的文字颜色,此项可以添加 css 颜色
color: ['red', 'yellow', 'blue', 'green', 'gray', 'white', 'black'],
// 图片的菜单项
img: ['换图', '宽度', '超链接', '预览图', '禁用预览', '上移', '下移', '删除'],
// 链接的菜单项
link: ['更换链接', '上移', '下移', '删除'],
// 音视频的菜单项
media: ['封面', '循环', '自动播放', '上移', '下移', '删除'],
// 卡片的菜单项
card: ['上移', '下移', '删除']
}
@@ -0,0 +1,813 @@
const path = require('path')
/* global getTop */
module.exports = {
style: `/* #ifndef MP-ALIPAY */
._address,
._article,
._aside,
._body,
._caption,
._center,
._cite,
._footer,
._header,
._html,
._nav,
._pre,
._section {
display: block;
}
/* #endif */`,
methods: {
/**
* @description 开始编辑文本
* @param {Event} e
*/
editStart (e) {
if (this.properties.opts[5]) {
const i = e.currentTarget.dataset.i
if (!this.data.ctrl['e' + i] && this.properties.opts[5] !== 'simple') {
// 显示虚线框
this.setData({
['ctrl.e' + i]: 1
})
// 点击其他地方则取消虚线框
setTimeout(() => {
this.root._mask.push(() => {
this.setData({
['ctrl.e' + i]: 0
})
})
}, 50)
this.root._edit = this
this.i = i
this.cursor = this.getNode(i).text.length
} else {
if (this.properties.opts[5] === 'simple') {
this.root._edit = this
this.i = i
this.cursor = this.getNode(i).text.length
}
this.root._mask.pop()
this.root._maskTap()
// 将 text 转为 textarea
this.setData({
['ctrl.e' + i]: 2
})
// 延时对焦,避免高度错误
setTimeout(() => {
this.setData({
['ctrl.e' + i]: 3
})
}, 50)
}
}
},
/**
* @description 输入文本
* @param {Event} e
*/
editInput (e) {
const i = e.target.dataset.i
// 替换连续空格
const value = e.detail.value.replace(/ {2,}/, $ => {
let res = '\xa0'
for (let i = 1; i < $.length; i++) {
res += '\xa0'
}
return res
})
this.root._editVal('nodes[' + (this.properties.opts[7] + i).replace(/_/g, '].children[') + '].text', this.getNode(i).text, value) // 记录编辑历史
this.cursor = e.detail.cursor
},
/**
* @description 完成编辑文本
* @param {Event} e
*/
editEnd (e) {
const i = e.target.dataset.i
// 更新到视图
this.setData({
['ctrl.e' + i]: 0
})
this.root.setData({
['nodes[' + (this.properties.opts[7] + i).replace(/_/g, '].children[') + '].text']: e.detail.value.replace(/ {2}/g, '\xa0 ')
})
if (e.detail.cursor !== undefined) {
this.cursor = e.detail.cursor
}
},
/**
* @description 插入一个标签
* @param {Object} node 要插入的标签
*/
insert (node) {
setTimeout(() => {
const arr = this.i.split('_')
const i = parseInt(arr.pop())
let path = arr.join('_')
const children = path ? this.getNode(path).children : this.properties.childs
const childs = children.slice(0)
if (!childs[i]) {
childs.push(node)
} else if (childs[i].text) {
// 在文本中插入
const text = childs[i].text
if (node.type === 'text') {
if (this.cursor) {
childs[i].text = text.substring(0, this.cursor) + node.text + text.substring(this.cursor)
} else {
childs[i].text += node.text
}
} else {
const list = []
if (this.cursor) {
list.push({
type: 'text',
text: text.substring(0, this.cursor)
})
}
list.push(node)
if (this.cursor < text.length) {
list.push({
type: 'text',
text: text.substring(this.cursor)
})
}
childs.splice(i, 1, ...list)
}
} else {
childs.splice(i + 1, 0, node)
}
path = this.properties.opts[7] + path
if (path[path.length - 1] === '_') {
path = path.slice(0, -1)
}
this.root._editVal('nodes' + (path ? '[' + path.replace(/_/g, '].children[') + '].children' : ''), children, childs, true)
this.i = arr.join('_') + '_' + (i + 1)
}, 200)
},
/**
* @description 移除第 i 个标签
* @param {Number} i
*/
remove (i) {
const arr = i.split('_')
const j = arr.pop()
let path = arr.join('_')
const children = path ? this.getNode(path).children : this.properties.childs
const childs = children.slice(0)
const delEle = childs.splice(j, 1)[0]
if (delEle.name === 'img' || delEle.name === 'video' || delEle.name === 'audio') {
let src = delEle.attrs.src
if (delEle.src) {
src = delEle.src.length === 1 ? delEle.src[0] : delEle.src
}
this.root.triggerEvent('remove', {
type: delEle.name,
src
})
}
this.root._edit = undefined
this.root._maskTap()
path = this.properties.opts[7] + path
if (path[path.length - 1] === '_') {
path = path.slice(0, -1)
}
this.root._editVal('nodes' + (path ? '[' + path.replace(/_/g, '].children[') + '].children' : ''), children, childs, true)
},
/**
* @description 标签被点击
* @param {Event} e
*/
nodeTap (e) {
if (this.properties.opts[5]) {
const i = e.currentTarget.dataset.i
if (this.root._table) {
const node = this.getNode(i)
if (node.name === 'table') {
this.root._table = undefined
this.root._remove_table = () => {
this.remove(i)
}
}
}
if (this.root._lock) return
// 阻止上层出现点击态
this.root._lock = true
setTimeout(() => {
this.root._lock = false
}, 50)
const node = this.getNode(i)
if (node.name === 'td' || node.name === 'th') {
this.root._table = true
}
if (this.data.ctrl['e' + this.i] === 3) return
this.root._maskTap()
this.root._edit = this
if (this.properties.opts[5] === 'simple') return
const arr = i.split('_')
const j = parseInt(arr.pop())
let path = arr.join('_')
const siblings = path ? this.getNode(path).children : this.properties.childs
// 显示实线框
this.setData({
['ctrl.e' + i]: 1
})
this.root._mask.push(() => {
this.setData({
['ctrl.e' + i]: 0
})
})
if (node.children.length === 1 && node.children[0].type === 'text') {
const ii = i + '_0'
if (!this.data.ctrl['e' + ii]) {
this.setData({
['ctrl.e' + ii]: 1
})
this.root._mask.push(() => {
this.setData({
['ctrl.e' + ii]: 0
})
})
this.cursor = node.children[0].text.length
}
this.i = ii
} else if (!(this.i || '').includes(i)) {
this.i = i + '_'
}
const items = this.root._getItem(node, j !== 0, j !== siblings.length - 1)
this.root._tooltip({
top: getTop(e),
items,
success: tapIndex => {
if (items[tapIndex] === '大小') {
// 改变字体大小
const style = node.attrs.style || ''
let value = style.match(/;font-size:([0-9]+)px/)
if (value) {
value = parseInt(value[1])
} else {
value = 16
}
this.root._slider({
min: 10,
max: 30,
value,
top: getTop(e),
changing: val => {
if (Math.abs(val - value) > 2) {
// 字号变换超过 2 时更新到视图
this.changeStyle('font-size', i, val + 'px', value + 'px')
value = e.detail.value
}
},
change: val => {
if (val !== value) {
this.changeStyle('font-size', i, val + 'px', value + 'px')
}
this.root._editVal('nodes[' + (this.properties.opts[7] + i).replace(/_/g, '].children[') + '].attrs.style', style, this.getNode(i).attrs.style)
}
})
} else if (items[tapIndex] === '颜色') {
// 改变文字颜色
const items = this.root._getItem('color')
this.root._color({
top: getTop(e),
items,
success: tapIndex => {
const style = node.attrs.style || ''
const value = style.match(/;color:([^;]+)/)
this.changeStyle('color', i, items[tapIndex], value ? value[1] : undefined)
this.root._editVal('nodes[' + (this.properties.opts[7] + i).replace(/_/g, '].children[') + '].attrs.style', style, this.getNode(i).attrs.style)
}
})
} else if (items[tapIndex] === '上移' || items[tapIndex] === '下移') {
const arr = siblings.slice(0)
const item = arr[j]
if (items[tapIndex] === '上移') {
arr[j] = arr[j - 1]
arr[j - 1] = item
} else {
arr[j] = arr[j + 1]
arr[j + 1] = item
}
path = this.properties.opts[7] + path
if (path[path.length - 1] === '_') {
path = path.slice(0, -1)
}
this.root._editVal('nodes' + (path ? '[' + path.replace(/_/g, '].children[') + '].children' : ''), siblings, arr, true)
} else if (items[tapIndex] === '删除') {
if ((node.name === 'td' || node.name === 'th') && this.root._remove_table) {
this.root._remove_table()
this.root._remove_table = undefined
} else {
this.remove(i)
}
} else {
const style = node.attrs.style || ''
let newStyle = ''
const item = items[tapIndex]
let name
let value
if (item === '斜体') {
name = 'font-style'
value = 'italic'
} else if (item === '粗体') {
name = 'font-weight'
value = 'bold'
} else if (item === '下划线') {
name = 'text-decoration'
value = 'underline'
} else if (item === '居中') {
name = 'text-align'
value = 'center'
} else if (item === '缩进') {
name = 'text-indent'
value = '2em'
}
if (style.includes(name + ':')) {
// 已有则取消
newStyle = style.replace(new RegExp(name + ':[^;]+'), '')
} else {
// 没有则添加
newStyle = style + ';' + name + ':' + value
}
this.root._editVal('nodes[' + (this.properties.opts[7] + i).replace(/_/g, '].children[') + '].attrs.style', style, newStyle, true)
}
}
})
}
},
/**
* @description 音视频被点击
* @param {Event} e
*/
mediaTap (e) {
if (this.properties.opts[5]) {
const i = e.target.dataset.i
const node = this.getNode(i)
const items = this.root._getItem(node)
this.root._maskTap()
this.root._edit = this
this.i = i
this.root._tooltip({
top: e.target.offsetTop - 30,
items,
success: tapIndex => {
switch (items[tapIndex]) {
case '封面':
// 设置封面
this.root.getSrc('img', node.attrs.poster || '').then(url => {
this.root._editVal('nodes[' + (this.properties.opts[7] + i).replace(/_/g, '].children[') + '].attrs.poster', node.attrs.poster, url instanceof Array ? url[0] : url, true)
}).catch(() => { })
break
case '删除':
this.remove(i)
break
case '循环':
case '不循环':
// 切换循环播放
this.root.setData({
['nodes[' + (this.properties.opts[7] + i).replace(/_/g, '].children[') + '].attrs.loop']: !node.attrs.loop
})
wx.showToast({
title: '成功'
})
break
case '自动播放':
case '不自动播放':
// 切换自动播放播放
this.root.setData({
['nodes[' + (this.properties.opts[7] + i).replace(/_/g, '].children[') + '].attrs.autoplay']: !node.attrs.autoplay
})
wx.showToast({
title: '成功'
})
break
}
}
})
// 避免上层出现点击态
this.root._lock = true
setTimeout(() => {
this.root._lock = false
}, 50)
}
},
/**
* 改变样式
* @param {String} name 属性名
* @param {Number} i 第几个标签
* @param {String} value 新值
* @param {String} oldVal 旧值
*/
changeStyle (name, i, value, oldVal) {
let style = this.getNode(i).attrs.style || ''
if (style.includes(';' + name + ':' + oldVal)) {
// style 中已经有
style = style.replace(';' + name + ':' + oldVal, ';' + name + ':' + value)
} else {
// 没有则新增
style += ';' + name + ':' + value
}
this.root.setData({
['nodes[' + (this.properties.opts[7] + i).replace(/_/g, '].children[') + '].attrs.style']: style
})
}
},
handler (file) {
if (file.isBuffer()) {
let content = file.contents.toString()
if (file.path.includes('miniprogram' + path.sep + 'index.wxml')) {
// 传递 editable 属性和路径
content = content.replace(/opts\s*=\s*"{{\[([^\]]+)\]}}"/, 'opts="{{[$1,editable,placeholder,\'\']}}"')
.replace(/<view(.*?)style\s*=\s*"{{containerStyle}}"/, '<view$1style="{{editable?\'min-height:200px;\':\'\'}}{{containerStyle}}" bindtap="_containTap"')
// 工具弹窗
.replace('</view>', ` <view wx:if="{{tooltip}}" class="_tooltip_contain" style="top:{{tooltip.top}}px">
<view class="_tooltip">
<view wx:for="{{tooltip.items}}" wx:key="index" class="_tooltip_item" data-i="{{index}}" bindtap="_tooltipTap">{{item}}</view>
</view>
</view>
<view wx:if="{{slider}}" class="_slider" style="top:{{slider.top}}px">
<slider value="{{slider.value}}" min="{{slider.min}}" max="{{slider.max}}" block-size="14" show-value activeColor="white" mp-alipay:style="padding:10px" bindchanging="_sliderChanging" bindchange="_sliderChange" />
</view>
<view wx:if="{{color}}" class="_tooltip_contain" style="top:{{color.top}}px">
<view class="_tooltip" style="overflow-y: hidden;">
<view wx:for="{{color.items}}" wx:key="index" class="_color_item" style="background-color:{{item}}" data-i="{{index}}" bindtap="_colorTap"></view>
</view>
</view>
</view>`)
} else if (file.path.includes('miniprogram' + path.sep + 'index.js')) {
// 添加 editable 属性,发生变化时重新解析
content = content.replace(/properties\s*:\s*{/, `properties: {
editable: {
type: null,
observer (val) {
if (this.properties.content) {
this.setContent(val ? this.properties.content : this.getContent())
} else if (val) {
this.setData({
nodes: [{
name: 'p',
attrs: {},
children: [{
type: 'text',
text: ''
}]
}]
})
// #ifdef MP-TOUTIAO
this.selectComponent('#_root', child => {
child.root = this
})
// #endif
}
if (!val) {
this._maskTap()
}
}
},
placeholder: String,`)
.replace(/didUpdate\s*\(e\)\s*{/, `didUpdate (e) {
if (e.editable !== this.properties.editable) {
const val = this.properties.editable
if (this.properties.content) {
this.setContent(val ? this.properties.content : this.getContent())
} else if (val) {
this.setData({
nodes: [{
name: 'p',
attrs: {},
children: [{
type: 'text',
text: ''
}]
}]
})
}
if (!val) {
this._maskTap()
}
}`)
// 处理各类弹窗的事件
.replace(/methods\s*:\s*{/, `methods: {
_containTap() {
if (!this._lock && !this.data.slider && !this.data.color) {
this._edit = undefined
this._maskTap()
}
},
_tooltipTap(e) {
this._tooltipcb(e.currentTarget.dataset.i)
this.setData({
tooltip: null
})
},
_sliderChanging(e) {
this._slideringcb(e.detail.value)
},
_sliderChange(e) {
this._slidercb(e.detail.value)
},
_colorTap(e) {
this._colorcb(e.currentTarget.dataset.i)
this.setData({
color: null
})
},`)
} else if (file.path.includes('miniprogram' + path.sep + 'index.wxss')) {
// 工具弹窗的样式
content += `/* 提示条 */
._tooltip_contain {
position: absolute;
right: 20px;
left: 20px;
text-align: center;
}
._tooltip {
box-sizing: border-box;
display: inline-block;
width: auto;
max-width: 100%;
height: 30px;
padding: 0 3px;
overflow: scroll;
font-size: 14px;
line-height: 30px;
white-space: nowrap;
}
._tooltip_item {
display: inline-block;
width: auto;
padding: 0 2vw;
line-height: 30px;
background-color: black;
color: white;
}
._color_item {
display: inline-block;
width: 18px;
height: 18px;
margin: 5px 2vw;
border:1px solid #dfe2e5;
border-radius: 50%;
}
/* 图片宽度滚动条 */
._slider {
position: absolute;
left: 20px;
width: 220px;
}
._tooltip,
._slider {
background-color: black;
border-radius: 3px;
opacity: 0.75;
}`
} else if (file.path.includes('parser.js')) {
content = content.replace(/popNode\s*=\s*function\s*\(\)\s*{/, 'popNode = function () {\n const editable = this.options.editable')
// 不转换标签名
.replace(/if\s*\(config.blockTags\[node.name\]\)\s*{[\s\S]+?}/, `if (config.blockTags[node.name]) {
if (!editable) {
node.name = 'div'
}
}`)
// 转换表格和列表
.replace(/node.c(\)|\s*&&|\s*\n)/g, '(node.c || editable)$1')
.replace(/while\s*\(map\[row\s*\+\s*'.'\s*\+\s*col\]\)\s*{[\s\S]+?}/, `while (map[row + '.' + col]) {
col++
}
if (editable) {
td.r = row
}`)
// 不做 expose 处理
.replace(/parser.prototype.expose\s*=\s*function\s*\(\)\s*{/, `parser.prototype.expose = function () {
if (this.options.editable) return`)
} else if (file.path.includes('node.wxml')) {
content = content.replace(/opts\s*=\s*"{{opts}}"/, 'opts="{{[opts[0],opts[1],opts[2],opts[3],opts[4],opts[5],opts[6],opts[7]+i+\'_\']}}"')
.replace(/opts\s*=\s*"{{opts}}"/, 'opts="{{[opts[0],opts[1],opts[2],opts[3],opts[4],opts[5],opts[6],opts[7]+i1+\'_\'+i2+\'_\'+i3+\'_\'+i4+\'_\'+i5+\'_\']}}"')
.replace('!n.c', "opts[5]?(!n.children||n.name=='a'):!n.c")
.replace(/!(n.?)\.c(?![a-z])/g, '(opts[5]?true:!$1.c)')
.replace(/isInline\((.*?)\)/g, '(opts[5]?true:isInline($1))')
// 修改普通标签
.replace(/<view\s*wx:else\s*id(.+?)style="/, '<view wx:else data-i="{{path+i}}" bindtap="nodeTap" id$1style="{{ctrl[\'e\'+path+i]&&opts[5]!==\'simple\'?\'border:1px solid black;padding:5px;display:block;\':\'\'}}')
.replace(/<view\s*wx:else\s*id(.+?)style="/, '<view wx:else data-i="{{\'\'+i1}}" bindtap="nodeTap" id$1style="{{ctrl[\'e\'+i1]&&opts[5]!==\'simple\'?\'border:1px solid black;padding:5px;display:block;\':\'\'}}')
.replace(/<view\s*wx:else\s*id(.+?)style="/, '<view wx:else data-i="{{i1+\'_\'+i2}}" bindtap="nodeTap" id$1style="{{ctrl[\'e\'+i1+\'_\'+i2]&&opts[5]!==\'simple\'?\'border:1px solid black;padding:5px;display:block;\':\'\'}}')
.replace(/<view\s*wx:else\s*id(.+?)style="/, '<view wx:else data-i="{{i1+\'_\'+i2+\'_\'+i3}}" bindtap="nodeTap" id$1style="{{ctrl[\'e\'+i1+\'_\'+i2+\'_\'+i3]&&opts[5]!==\'simple\'?\'border:1px solid black;padding:5px;display:block;\':\'\'}}')
.replace(/<view\s*wx:else\s*id(.+?)style="/, '<view wx:else data-i="{{i1+\'_\'+i2+\'_\'+i3+\'_\'+i4}}" bindtap="nodeTap" id$1style="{{ctrl[\'e\'+i1+\'_\'+i2+\'_\'+i3+\'_\'+i4]&&opts[5]!==\'simple\'?\'border:1px solid black;padding:5px;display:block;\':\'\'}}')
// 修改文本块
.replace(/<!--\s*文本\s*-->[\s\S]+?<!--\s*链接\s*-->/,
`<block wx:elif="{{n.type==='text'}}">
<text wx:if="{{!ctrl['e'+i]}}" data-i="{{i}}" mp-weixin:user-select="{{opts[4]}}" decode="{{!opts[5]}}" bindtap="editStart">{{n.text}}
<text wx:if="{{!n.text}}" style="color:gray">{{opts[6]||'请输入'}}</text>
</text>
<text wx:elif="{{ctrl['e'+i]===1}}" data-i="{{i}}" style="border:1px dashed black;min-width:50px;width:auto;padding:5px;display:block" catchtap="editStart">{{n.text}}
<text wx:if="{{!n.text}}" style="color:gray">{{opts[6]||'请输入'}}</text>
</text>
<textarea wx:else style="{{opts[5]==='simple'?'':'border:1px dashed black;'}}min-width:50px;width:auto;padding:5px" auto-height maxlength="-1" focus="{{ctrl['e'+i]===3}}" value="{{n.text}}" data-i="{{i}}" bindinput="editInput" bindblur="editEnd" />
</block>
<text wx:elif="{{n.name==='br'}}">\\n</text>`)
// 修改图片
.replace(/<image(.+?)id="\{\{n.attrs.id/, '<image$1id="{{n.attrs.id||(\'n\'+i)')
.replace('height:1px', "height:{{ctrl['h'+i]||1}}px")
.replace('style="{{ctrl[i]', 'style="{{ctrl[\'e\'+i]&&opts[5]!==\'simple\'?\'border:1px dashed black;padding:3px;\':\'\'}}{{ctrl[i]')
.replace(/weixin:show-menu-by-longpress\s*=\s*"{{(\S+?)}}"\s*baidu:image-menu-prevent\s*=\s*"{{(\S+?)}}"/, 'weixin:show-menu-by-longpress="{{!opts[5]&&$1}}" baidu:image-menu-prevent="{{opts[5]||$2}}"')
// 修改音视频
.replace('<video', '<video bindtap="mediaTap"')
.replace('audio ', 'audio bindtap="mediaTap" ')
.replace('card', 'card bindtap="mediaTap"')
} else if (file.path.includes('node.js') && file.extname === '.js') {
content = `
const Parser = require('../parser')
function getTop(e) {
let top
// #ifndef MP-ALIPAY
top = e.detail.y
// #endif
// #ifdef MP-ALIPAY
top = top = e.detail.pageY
// #endif
if (top - e.currentTarget.offsetTop < 150 || top < 600) {
top = e.currentTarget.offsetTop
}
if (top < 30) {
top += 70
}
return top - 30
}` + content.replace('methods:', `detached () {
if (this.root && this.root._edit === this) {
this.root._edit = undefined
}
},
methods:`)
// 记录图片宽度
.replace(/imgLoad\s*\(e\)\s*{/, `imgLoad (e) {
// #ifdef MP-WEIXIN || MP-QQ
if (this.properties.opts[5]) {
setTimeout(() => {
const id = this.getNode(i).attrs.id || ('n' + i)
wx.createSelectorQuery().in(this).select('#' + id).boundingClientRect().exec(res => {
this.setData({
['ctrl.h'+i]: res[0].height
})
})
}, 50)
}
// #endif`)
.replace(/if\s*\(!node.w\)\s*{[\s\S]+?}/,
`if (!node.w) {
val = e.detail.width
if (this.properties.opts[5]) {
const data = {}
const path = 'nodes[' + (this.properties.opts[7] + i).replace(/_/g, '].children[') + '].attrs.'
if (val < 150) {
data[path + 'ignore'] = 'T'
}
data[path + 'width'] = val.toString()
this.root.setData(data)
}
}`)
// 处理图片点击
.replace(/imgTap\s*\(e\)\s*{([\s\S]+?)},\s*\/\*/,
`imgTap (e) {
if (!this.properties.opts[5]) {$1} else {
const i = e.target.dataset.i
const node = this.getNode(i)
const items = this.root._getItem(node)
this.root._edit = this
const parser = new Parser(this.root)
this.i = i
this.root._maskTap()
this.setData({
['ctrl.e' + i]: 1
})
this.root._mask.push(() => {
this.setData({
['ctrl.e' + i]: 0
})
})
this.root._tooltip({
top: getTop(e),
items,
success: tapIndex => {
if (items[tapIndex] === '换图') {
// 换图
this.root.getSrc('img', node.attrs.src || '').then(url => {
this.root._editVal('nodes[' + (this.properties.opts[7] + i).replace(/_/g, '].children[') + '].attrs.src', node.attrs.src, parser.getUrl(url instanceof Array ? url[0] : url), true)
}).catch(() => { })
} else if (items[tapIndex] === '宽度') {
// 更改宽度
const style = node.attrs.style || ''
let value = style.match(/max-width:([0-9]+)%/)
if (value) {
value = parseInt(value[1])
} else {
value = 100
}
this.root._slider({
min: 0,
max: 100,
value,
top: getTop(e),
changing: val => {
// 变化超过 5% 更新时视图
if (Math.abs(val - value) > 5) {
this.changeStyle('max-width', i, val + '%', value + '%')
value = val
}
},
change: val => {
if (val !== value) {
this.changeStyle('max-width', i, val + '%', value + '%')
value = val
}
this.root._editVal('nodes[' + (this.properties.opts[7] + i).replace(/_/g, '].children[') + '].attrs.style', style, this.getNode(i).attrs.style)
}
})
} else if (items[tapIndex] === '超链接') {
// 将图片设置为链接
this.root.getSrc('link', node.a ? node.a.href : '').then(url => {
// 如果有 a 标签则替换 href
if (node.a) {
this.root._editVal('nodes[' + (this.properties.opts[7] + i).replace(/_/g, '].children[') + '].a.href', node.a.href, parser.getUrl(url), true)
} else {
const link = {
name: 'a',
attrs: {
href: parser.getUrl(url)
},
children: [node]
}
node.a = link.attrs
this.root._editVal('nodes[' + (this.properties.opts[7] + i).replace(/_/g, '].children[') + ']', node, link, true)
}
wx.showToast({
title: '成功'
})
}).catch(() => { })
} else if (items[tapIndex] === '预览图') {
// 设置预览图链接
this.root.getSrc('img', node.attrs['original-src'] || '').then(url => {
this.root._editVal('nodes[' + (this.properties.opts[7] + i).replace(/_/g, '].children[') + '].attrs.original-src', node.attrs['original-src'], parser.getUrl(url instanceof Array ? url[0] : url), true)
wx.showToast({
title: '成功'
})
}).catch(() => { })
} else if (items[tapIndex] === '删除') {
this.remove(i)
} else {
// 禁用 / 启用预览
this.root.setData({
['nodes[' + (this.properties.opts[7] + i).replace(/_/g, '].children[') + '].attrs.ignore']: !node.attrs.ignore
})
wx.showToast({
title: '成功'
})
}
}
})
this.root._lock = true
setTimeout(() => {
this.root._lock = false
}, 50)
}
},
/*`)
// 处理链接点击
.replace(/linkTap\s*\(e\)\s*{([\s\S]+?)},\s*\/\*/,
`linkTap (e) {
if (!this.properties.opts[5]) {$1} else {
const i = e.currentTarget.dataset.i
const node = this.getNode(i)
const items = this.root._getItem(node)
this.root._tooltip({
top: getTop(e),
items,
success: tapIndex => {
if (items[tapIndex] === '更换链接') {
this.root.getSrc('link', node.attrs.href).then(url => {
this.root._editVal('nodes[' + (this.properties.opts[7] + i).replace(/_/g, '].children[') + '].attrs.href', node.attrs.href, url, true)
wx.showToast({
title: '成功'
})
}).catch(() => { })
} else {
this.remove(i)
}
}
})
}
},
/*`)
}
file.contents = Buffer.from(content)
}
}
}
@@ -0,0 +1,551 @@
/**
* @fileoverview editable 插件
*/
const config = require('./config')
const Parser = require('../parser')
function Editable (vm) {
this.vm = vm
this.editHistory = [] // 历史记录
this.editI = -1 // 历史记录指针
vm._mask = [] // 蒙版被点击时进行的操作
/**
* @description 移动历史记录指针
* @param {Number} num 移动距离
*/
const move = num => {
const item = this.editHistory[this.editI + num]
if (item) {
this.editI += num
vm.setData({
[item.key]: item.value
})
}
}
vm.undo = () => move(-1) // 撤销
vm.redo = () => move(1) // 重做
/**
* @description 更新记录
* @param {String} path 路径
* @param {*} oldVal 旧值
* @param {*} newVal 新值
* @param {Boolean} set 是否更新到视图
* @private
*/
vm._editVal = (path, oldVal, newVal, set) => {
// 当前指针后的内容去除
while (this.editI < this.editHistory.length - 1) {
this.editHistory.pop()
}
// 最多存储 30 条操作记录
while (this.editHistory.length > 30) {
this.editHistory.pop()
this.editI--
}
const last = this.editHistory[this.editHistory.length - 1]
if (!last || last.key !== path) {
if (last) {
// 去掉上一次的新值
this.editHistory.pop()
this.editI--
}
// 存入这一次的旧值
this.editHistory.push({
key: path,
value: oldVal
})
this.editI++
}
// 存入本次的新值
this.editHistory.push({
key: path,
value: newVal
})
this.editI++
// 更新到视图
if (set) {
vm.setData({
[path]: newVal
})
}
}
/**
* @description 获取菜单项
* @private
*/
vm._getItem = function (node, up, down) {
let items
let i
if (node === 'color') {
return config.color
}
if (node.name === 'img') {
items = config.img.slice(0)
if (!vm.getSrc) {
i = items.indexOf('换图')
if (i !== -1) {
items.splice(i, 1)
}
i = items.indexOf('超链接')
if (i !== -1) {
items.splice(i, 1)
}
i = items.indexOf('预览图')
if (i !== -1) {
items.splice(i, 1)
}
}
i = items.indexOf('禁用预览')
if (i !== -1 && node.attrs.ignore) {
items[i] = '启用预览'
}
} else if (node.name === 'a') {
items = config.link.slice(0)
if (!vm.getSrc) {
i = items.indexOf('更换链接')
if (i !== -1) {
items.splice(i, 1)
}
}
} else if (node.name === 'video' || node.name === 'audio') {
items = config.media.slice(0)
i = items.indexOf('封面')
if (!vm.getSrc && i !== -1) {
items.splice(i, 1)
}
i = items.indexOf('循环')
if (node.attrs.loop && i !== -1) {
items[i] = '不循环'
}
i = items.indexOf('自动播放')
if (node.attrs.autoplay && i !== -1) {
items[i] = '不自动播放'
}
} else if (node.name === 'card') {
items = config.card.slice(0)
} else {
items = config.node.slice(0)
}
if (!up) {
i = items.indexOf('上移')
if (i !== -1) {
items.splice(i, 1)
}
}
if (!down) {
i = items.indexOf('下移')
if (i !== -1) {
items.splice(i, 1)
}
}
return items
}
/**
* @description 显示 tooltip
* @param {object} obj
* @private
*/
vm._tooltip = function (obj) {
vm.setData({
tooltip: {
top: obj.top,
items: obj.items
}
})
vm._tooltipcb = obj.success
}
/**
* @description 显示滚动条
* @param {object} obj
* @private
*/
vm._slider = function (obj) {
vm.setData({
slider: {
min: obj.min,
max: obj.max,
value: obj.value,
top: obj.top
}
})
vm._slideringcb = obj.changing
vm._slidercb = obj.change
}
/**
* @description 显示颜色选择
* @param {object} obj
* @private
*/
vm._color = function (obj) {
vm.setData({
color: {
items: obj.items,
top: obj.top
}
})
vm._colorcb = obj.success
}
/**
* @description 点击蒙版
* @private
*/
vm._maskTap = function () {
// 隐藏所有悬浮窗
while (this._mask.length) {
(this._mask.pop())()
}
const data = {}
if (this.data.tooltip) {
data.tooltip = null
}
if (this.data.slider) {
data.slider = null
}
if (this.data.color) {
data.color = null
}
if (this.data.tooltip || this.data.slider || this.data.color) {
this.setData(data)
}
}
/**
* @description 插入节点
* @param {Object} node
*/
function insert (node) {
if (vm._edit) {
vm._edit.insert(node)
} else {
const nodes = vm.data.nodes.slice(0)
nodes.push(node)
vm._editVal('nodes', vm.data.nodes, nodes, true)
}
}
/**
* @description 在光标处插入指定 html 内容
* @param {String} html 内容
*/
vm.insertHtml = html => {
this.inserting = true
const arr = new Parser(vm).parse(html)
this.inserting = undefined
for (let i = 0; i < arr.length; i++) {
insert(arr[i])
}
}
/**
* @description 在光标处插入图片
*/
vm.insertImg = function () {
vm.getSrc && vm.getSrc('img').then(src => {
if (typeof src === 'string') {
src = [src]
}
const parser = new Parser(vm)
for (let i = 0; i < src.length; i++) {
insert({
name: 'img',
attrs: {
src: parser.getUrl(src[i])
}
})
}
}).catch(() => { })
}
/**
* @description 在光标处插入一个链接
*/
vm.insertLink = function () {
vm.getSrc && vm.getSrc('link').then(url => {
insert({
name: 'a',
attrs: {
href: url
},
children: [{
type: 'text',
text: url
}]
})
}).catch(() => { })
}
/**
* @description 在光标处插入一个表格
* @param {Number} rows 行数
* @param {Number} cols 列数
*/
vm.insertTable = function (rows, cols) {
const table = {
name: 'table',
attrs: {
style: 'display:table;width:100%;margin:10px 0;text-align:center;border-spacing:0;border-collapse:collapse;border:1px solid gray'
},
children: []
}
for (let i = 0; i < rows; i++) {
const tr = {
name: 'tr',
attrs: {},
children: []
}
for (let j = 0; j < cols; j++) {
tr.children.push({
name: 'td',
attrs: {
style: 'padding:2px;border:1px solid gray'
},
children: [{
type: 'text',
text: ''
}]
})
}
table.children.push(tr)
}
insert(table)
}
/**
* @description 插入视频/音频
* @param {Object} node
*/
function insertMedia (node) {
if (typeof node.src === 'string') {
node.src = [node.src]
}
const parser = new Parser(vm)
// 拼接主域名
for (let i = 0; i < node.src.length; i++) {
node.src[i] = parser.getUrl(node.src[i])
}
insert({
name: 'div',
attrs: {
style: 'text-align:center'
},
children: [node]
})
}
/**
* @description 在光标处插入一个视频
*/
vm.insertVideo = function () {
vm.getSrc && vm.getSrc('video').then(src => {
insertMedia({
name: 'video',
attrs: {
controls: 'T'
},
src
})
}).catch(() => { })
}
/**
* @description 在光标处插入一个音频
*/
vm.insertAudio = function () {
vm.getSrc && vm.getSrc('audio').then(attrs => {
let src
if (attrs.src) {
src = attrs.src
attrs.src = undefined
} else {
src = attrs
attrs = {}
}
attrs.controls = 'T'
insertMedia({
name: 'audio',
attrs,
src
})
}).catch(() => { })
}
/**
* @description 在光标处插入一段文本
*/
vm.insertText = function () {
insert({
name: 'p',
attrs: {},
children: [{
type: 'text',
text: ''
}]
})
}
/**
* @description 清空内容
*/
vm.clear = function () {
vm._maskTap()
vm._edit = undefined
vm.setData({
nodes: [{
name: 'p',
attrs: {},
children: [{
type: 'text',
text: ''
}]
}]
})
}
/**
* @description 获取编辑后的 html
*/
vm.getContent = function () {
let html = '';
// 递归遍历获取
(function traversal (nodes, table) {
for (let i = 0; i < nodes.length; i++) {
let item = nodes[i]
if (item.type === 'text') {
// 编码实体
html += item.text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br>').replace(/\xa0/g, '&nbsp;')
} else {
// 还原被转换的 svg
if (item.name === 'img' && (item.attrs.src || '').includes('data:image/svg+xml;utf8,')) {
html += item.attrs.src.substr(24).replace(/%23/g, '#').replace('<svg', '<svg style="' + (item.attrs.style || '') + '"')
continue
} else if (item.name === 'video' || item.name === 'audio') {
// 还原 video 和 audio 的 source
if (item.src.length > 1) {
item.children = []
for (let j = 0; j < item.src.length; j++) {
item.children.push({
name: 'source',
attrs: {
src: item.src[j]
}
})
}
} else {
item.attrs.src = item.src[0]
}
} else if (item.name === 'div' && (item.attrs.style || '').includes('overflow:auto') && (item.children[0] || {}).name === 'table') {
// 还原滚动层
item = item.children[0]
}
// 还原 table
if (item.name === 'table') {
table = item.attrs
if ((item.attrs.style || '').includes('display:grid')) {
item.attrs.style = item.attrs.style.split('display:grid')[0]
const children = [{
name: 'tr',
attrs: {},
children: []
}]
for (let j = 0; j < item.children.length; j++) {
item.children[j].attrs.style = item.children[j].attrs.style.replace(/grid-[^;]+;*/g, '')
if (item.children[j].r !== children.length) {
children.push({
name: 'tr',
attrs: {},
children: [item.children[j]]
})
} else {
children[children.length - 1].children.push(item.children[j])
}
}
item.children = children
}
}
html += '<' + item.name
for (const attr in item.attrs) {
let val = item.attrs[attr]
if (!val) continue
// bool 型省略值
if (val === 'T' || val === true) {
html += ' ' + attr
continue
} else if (item.name[0] === 't' && attr === 'style' && table) {
// 取消为了显示 table 添加的 style
val = val.replace(/;*display:table[^;]*/, '')
if (table.border) {
val = val.replace(/border[^;]+;*/g, $ => $.includes('collapse') ? $ : '')
}
if (table.cellpadding) {
val = val.replace(/padding[^;]+;*/g, '')
}
if (!val) continue
}
html += ' ' + attr + '="' + val.replace(/"/g, '&quot;') + '"'
}
html += '>'
if (item.children) {
traversal(item.children, table)
html += '</' + item.name + '>'
}
}
}
})(vm.data.nodes)
// 其他插件处理
for (let i = vm.plugins.length; i--;) {
if (vm.plugins[i].onGetContent) {
html = vm.plugins[i].onGetContent(html) || html
}
}
return html
}
}
Editable.prototype.onUpdate = function (content, config) {
if (this.vm.properties.editable) {
this.vm._maskTap()
config.entities.amp = '&'
if (!this.inserting) {
this.vm._edit = undefined
if (!content) {
setTimeout(() => {
this.vm.setData({
nodes: [{
name: 'p',
attrs: {},
children: [{
type: 'text',
text: ''
}]
}]
})
}, 0)
}
}
}
}
Editable.prototype.onParse = function (node) {
// 空白单元格可编辑
if (this.vm.properties.editable && (node.name === 'td' || node.name === 'th') && !this.vm.getText(node.children)) {
node.children.push({
type: 'text',
text: ''
})
}
}
module.exports = Editable
+744
View File
@@ -0,0 +1,744 @@
/* global getTop */
module.exports = {
style: `/* #ifndef H5 || MP-ALIPAY || APP-PLUS */
._address,
._article,
._aside,
._body,
._caption,
._center,
._cite,
._footer,
._header,
._html,
._nav,
._pre,
._section {
display: block;
}
/* #endif */
._video {
width: 300px;
height: 225px;
display: inline-block;
background-color: black;
}`,
methods: {
/**
* @description 开始编辑文本
* @param {Event} e
*/
editStart (e) {
if (this.opts[5]) {
const i = e.currentTarget.dataset.i
if (!this.ctrl['e' + i] && this.opts[5] !== 'simple') {
// 显示虚线框
this.$set(this.ctrl, 'e' + i, 1)
setTimeout(() => {
this.root._mask.push(() => this.$set(this.ctrl, 'e' + i, 0))
}, 50)
this.root._edit = this
this.i = i
this.cursor = this.childs[i].text.length
} else {
if (this.opts[5] === 'simple') {
this.root._edit = this
this.i = i
this.cursor = this.childs[i].text.length
}
this.root._mask.pop()
this.root._maskTap()
// 将 text 转为 textarea
this.$set(this.ctrl, 'e' + i, 2)
// 延时对焦,避免高度错误
setTimeout(() => {
this.$set(this.ctrl, 'e' + i, 3)
}, 50)
}
}
},
/**
* @description 输入文本
* @param {Event} e
*/
editInput (e) {
const i = e.target.dataset.i
// 替换连续空格
const value = e.detail.value.replace(/ {2,}/, $ => {
let res = '\xa0'
for (let i = 1; i < $.length; i++) {
res += '\xa0'
}
return res
})
this.root._editVal(`${this.opts[7]}.${i}.text`, this.childs[i].text, value) // 记录编辑历史
this.cursor = e.detail.cursor
},
/**
* @description 完成编辑文本
* @param {Event} e
*/
editEnd (e) {
const i = e.target.dataset.i
this.$set(this.ctrl, 'e' + i, 0)
// 更新到视图
this.root._setData(`${this.opts[7]}.${i}.text`, e.detail.value.replace(/ {2}/g, '\xa0 '))
if (e.detail.cursor !== undefined) {
this.cursor = e.detail.cursor
}
},
/**
* @description 插入一个标签
* @param {Object} node 要插入的标签
*/
insert (node) {
setTimeout(() => {
const childs = this.childs.slice(0)
if (!childs[this.i]) {
childs.push(node)
} else if (childs[this.i].text) {
// 在文本中插入
const text = childs[this.i].text
if (node.type === 'text') {
if (this.cursor) {
childs[this.i].text = text.substring(0, this.cursor) + node.text + text.substring(this.cursor)
} else {
childs[this.i].text += node.text
}
} else {
const list = []
if (this.cursor) {
list.push({
type: 'text',
text: text.substring(0, this.cursor)
})
}
list.push(node)
if (this.cursor < text.length) {
list.push({
type: 'text',
text: text.substring(this.cursor)
})
}
childs.splice(this.i, 1, ...list)
}
} else {
childs.splice(parseInt(this.i) + 1, 0, node)
}
this.root._editVal(this.opts[7], this.childs, childs, true)
this.i = parseInt(this.i) + 1
}, 200)
},
/**
* @description 移除第 i 个标签
* @param {Number} i
*/
remove (i) {
const arr = this.childs.slice(0)
const delEle = arr.splice(i, 1)[0]
if (delEle.name === 'img' || delEle.name === 'video' || delEle.name === 'audio') {
let src = delEle.attrs.src
if (delEle.src) {
src = delEle.src.length === 1 ? delEle.src[0] : delEle.src
}
this.root.$emit('remove', {
type: delEle.name,
src
})
}
this.root._edit = undefined
this.root._maskTap()
this.root._editVal(this.opts[7], this.childs, arr, true)
},
/**
* @description 标签被点击
* @param {Event} e
*/
nodeTap (e) {
if (this.opts[5]) {
if (this.root._lock) return
this.root._lock = true
setTimeout(() => {
this.root._lock = false
}, 50)
if (this.ctrl['e' + this.i] === 3) return
this.root._maskTap()
this.root._edit = this
if (this.opts[5] === 'simple') return
let start = this.opts[7].lastIndexOf('children.')
if (start !== -1) {
start += 9
} else {
start = 6
}
const i = parseInt(this.opts[7].substring(start, this.opts[7].lastIndexOf('.children')))
let parent = this.$parent
while (parent && parent.$options.name !== 'node') {
parent = parent.$parent
}
let remove = () => {
parent.remove(i)
}
if (this.opts[7].length - parent.opts[7].length > 15) {
const parts = this.opts[7].split('.')
let childs = parent.childs
const i = parseInt(parts[parent.opts[7].split('.').length])
const oldParent = parent
// 删除整个表格
remove = () => {
oldParent.remove(i)
}
for (let i = parent.opts[7].split('.').length; i < parts.length - 2; i++) {
childs = childs[parts[i]]
}
const that = this
parent = {
childs,
opts: [undefined, undefined, undefined, undefined, undefined, undefined, undefined, parts.slice(0, parts.length - 2).join('.')],
changeStyle (name, i, value, oldVal) {
let style = this.childs[i].attrs.style || ''
if (style.includes(';' + name + ':' + oldVal)) {
style = style.replace(';' + name + ':' + oldVal, ';' + name + ':' + value)
} else {
style += ';' + name + ':' + value
}
that.root._setData(`${this.opts[7]}.${i}.attrs.style`, style)
}
}
}
if (!parent) return
// 显示实线框
this.$set(this.ctrl, 'root', 1)
this.root._mask.push(() => this.$set(this.ctrl, 'root', 0))
if (this.childs.length === 1 && this.childs[0].type === 'text' && !this.ctrl.e0) {
this.$set(this.ctrl, 'e0', 1)
this.root._mask.push(() => this.$set(this.ctrl, 'e0', 0))
this.i = 0
this.cursor = this.childs[0].text.length
}
const items = this.root._getItem(parent.childs[i], i !== 0, i !== parent.childs.length - 1)
this.root._tooltip({
top: getTop(e),
items,
success: tapIndex => {
if (items[tapIndex] === '大小') {
// 改变字体大小
const style = parent.childs[i].attrs.style || ''
let value = style.match(/;font-size:([0-9]+)px/)
if (value) {
value = parseInt(value[1])
} else {
value = 16
}
this.root._slider({
min: 10,
max: 30,
value,
top: getTop(e),
changing: val => {
if (Math.abs(val - value) > 2) {
// 字号变换超过 2 时更新到视图
parent.changeStyle('font-size', i, val + 'px', value + 'px')
value = e.detail.value
}
},
change: val => {
if (val !== value) {
parent.changeStyle('font-size', i, val + 'px', value + 'px')
}
this.root._editVal(`${parent.opts[7]}.${i}.attrs.style`, style, parent.childs[i].attrs.style)
}
})
} else if (items[tapIndex] === '颜色') {
// 改变文字颜色
const items = this.root._getItem('color')
this.root._color({
top: getTop(e),
items,
success: tapIndex => {
const style = parent.childs[i].attrs.style || ''
const value = style.match(/;color:([^;]+)/)
parent.changeStyle('color', i, items[tapIndex], value ? value[1] : undefined)
this.root._editVal(`${parent.opts[7]}.${i}.attrs.style`, style, parent.childs[i].attrs.style)
}
})
} else if (items[tapIndex] === '上移' || items[tapIndex] === '下移') {
const arr = parent.childs.slice(0)
const item = arr[i]
if (items[tapIndex] === '上移') {
arr[i] = arr[i - 1]
arr[i - 1] = item
} else {
arr[i] = arr[i + 1]
arr[i + 1] = item
}
this.root._editVal(parent.opts[7], parent.childs, arr, true)
} else if (items[tapIndex] === '删除') {
remove()
} else {
const style = parent.childs[i].attrs.style || ''
let newStyle = ''
const item = items[tapIndex]
let name
let value
if (item === '斜体') {
name = 'font-style'
value = 'italic'
} else if (item === '粗体') {
name = 'font-weight'
value = 'bold'
} else if (item === '下划线') {
name = 'text-decoration'
value = 'underline'
} else if (item === '居中') {
name = 'text-align'
value = 'center'
} else if (item === '缩进') {
name = 'text-indent'
value = '2em'
}
if (style.includes(name + ':')) {
// 已有则取消
newStyle = style.replace(new RegExp(name + ':[^;]+'), '')
} else {
// 没有则添加
newStyle = style + ';' + name + ':' + value
}
this.root._editVal(`${parent.opts[7]}.${i}.attrs.style`, style, newStyle, true)
}
}
})
}
},
/**
* @description 音视频被点击
* @param {Event} e
*/
mediaTap (e, index) {
if (this.opts[5]) {
const i = e.target.dataset.i || index
const node = this.childs[i]
const items = this.root._getItem(node)
this.root._maskTap()
this.root._edit = this
this.i = i
this.root._tooltip({
top: e.currentTarget.offsetTop - 30,
items,
success: tapIndex => {
switch (items[tapIndex]) {
case '封面':
// 设置封面
this.root.getSrc('img', node.attrs.poster || '').then(url => {
this.root._editVal(`${this.opts[7]}.${i}.attrs.poster`, node.attrs.poster, url instanceof Array ? url[0] : url, true)
}).catch(() => { })
break
case '删除':
this.remove(i)
break
case '循环':
case '不循环':
// 切换循环播放
this.root._setData(`${this.opts[7]}.${i}.attrs.loop`, !node.attrs.loop)
uni.showToast({
title: '成功'
})
break
case '自动播放':
case '不自动播放':
// 切换自动播放播放
this.root._setData(`${this.opts[7]}.${i}.attrs.autoplay`, !node.attrs.autoplay)
uni.showToast({
title: '成功'
})
break
}
}
})
// 避免上层出现点击态
this.root._lock = true
setTimeout(() => {
this.root._lock = false
}, 50)
}
},
/**
* 改变样式
* @param {String} name 属性名
* @param {Number} i 第几个标签
* @param {String} value 新值
* @param {String} oldVal 旧值
*/
changeStyle (name, i, value, oldVal) {
let style = this.childs[i].attrs.style || ''
if (style.includes(';' + name + ':' + oldVal)) {
// style 中已经有
style = style.replace(';' + name + ':' + oldVal, ';' + name + ':' + value)
} else {
// 没有则新增
style += ';' + name + ':' + value
}
this.root._setData(`${this.opts[7]}.${i}.attrs.style`, style)
}
},
handler (file) {
if (file.isBuffer()) {
let content = file.contents.toString()
if (file.path.includes('mp-html.vue')) {
// 传递 editable 属性和路径
content = content.replace(/opts\s*=\s*"\[([^\]]+)\]"/, 'opts="[$1,editable,placeholder,\'nodes\']"')
.replace(/<view(.*?):style\s*=\s*"containerStyle"/, '<view$1:style="(editable?\'min-height:200px;\':\'\')+containerStyle" @tap="_containTap"')
// 工具弹窗
.replace(/<\/view>\s*<\/template>/, ` <view v-if="tooltip" class="_tooltip_contain" :style="'top:'+tooltip.top+'px'">
<view class="_tooltip">
<view v-for="(item, index) in tooltip.items" v-bind:key="index" class="_tooltip_item" :data-i="index" @tap="_tooltipTap">{{item}}</view>
</view>
</view>
<view v-if="slider" class="_slider" :style="'top:'+slider.top+'px'">
<slider :value="slider.value" :min="slider.min" :max="slider.max" handle-size="14" block-size="14" show-value activeColor="white" style="padding:3px" @changing="_sliderChanging" @change="_sliderChange" />
</view>
<view v-if="color" class="_tooltip_contain" :style="'top:'+color.top+'px'">
<view class="_tooltip" style="overflow-y: hidden;">
<view v-for="(item, index) in color.items" v-bind:key="index" class="_color_item" :style="'background-color:'+item" :data-i="index" @tap="_colorTap"></view>
</view>
</view>
</view>
</template>`)
// 添加 data
.replace(/data\s*\(\)\s*{\s*return\s*{/, `data() {
return {
tooltip: null,
slider: null,
color: null,`)
// 添加 editable 属性
.replace(/props\s*:\s*{/, `props: {
editable: [Boolean, String],
placeholder: String,`)
// 添加 watch
.replace(/watch\s*:\s*{/, `watch: {
editable(val) {
this.setContent(val ? this.content : this.getContent())
if (!val)
this._maskTap()
},`)
.replace(/if\s*\(this.content/, 'if ((this.content || this.editable)')
// 处理各类弹窗的事件
.replace(/methods\s*:\s*{/, `methods: {
_containTap() {
if (!this._lock && !this.slider && !this.color) {
this._edit = undefined
this._maskTap()
}
},
_tooltipTap(e) {
this._tooltipcb(e.currentTarget.dataset.i)
this.$set(this, 'tooltip', null)
},
_sliderChanging(e) {
this._slideringcb(e.detail.value)
},
_sliderChange(e) {
this._slidercb(e.detail.value)
},
_colorTap(e) {
this._colorcb(e.currentTarget.dataset.i)
this.$set(this, 'color', null)
},`)
// 工具弹窗的样式
.replace('</style>', `
/* 提示条 */
._tooltip_contain {
position: absolute;
right: 20px;
left: 20px;
text-align: center;
}
._tooltip {
box-sizing: border-box;
display: inline-block;
width: auto;
max-width: 100%;
height: 30px;
padding: 0 3px;
overflow: scroll;
font-size: 14px;
line-height: 30px;
white-space: nowrap;
}
._tooltip_item {
display: inline-block;
width: auto;
padding: 0 2vw;
line-height: 30px;
background-color: black;
color: white;
}
._color_item {
display: inline-block;
width: 18px;
height: 18px;
margin: 5px 2vw;
border:1px solid #dfe2e5;
border-radius: 50%;
}
/* 图片宽度滚动条 */
._slider {
position: absolute;
left: 20px;
width: 220px;
}
._tooltip,
._slider {
background-color: black;
border-radius: 3px;
opacity: 0.75;
}
</style>`)
} else if (file.path.includes('parser.js')) {
// 不做 expose 处理
content = content.replace(/parser.prototype.expose\s*=\s*function\s*\(\)\s*{/, `parser.prototype.expose = function () {
if (this.options.editable) return`)
.replace(/popNode\s*=\s*function\s*\(\)\s*{/, 'popNode = function () {\n const editable = this.options.editable')
// 不转换标签名
.replace(/if\s*\(config.blockTags\[node.name\]\)\s*{[\s\S]+?}/, `if (config.blockTags[node.name]) {
if (!editable) {
node.name = 'div'
}
}`)
// 转换表格和列表
.replace(/else\s*if\s*\(node.c\)/, 'else if (!editable && node.c )')
.replace(/node.c(\)|\s*&&|\s*\n)/g, '(node.c || editable)$1')
.replace(/while\s*\(map\[row\s*\+\s*'.'\s*\+\s*col\]\)\s*{[\s\S]+?}/, `while (map[row + '.' + col]) {
col++
}
if (editable) {
td.r = row
}`)
.replace(/let\s+str\s*=\s*'<video style="width:100%;height:100%"'/, `let str = '<video style="width:100%;height:100%"'
if (editable) {
attrs.controls = ''
}`)
} else if (file.path.includes('node.vue')) {
content =
// 传递 opts
content.replace(/:childs\s*=\s*"tbody.children"\s*:opts="opts"/, ':childs="tbody.children" :opts="[opts[0],opts[1],opts[2],opts[3],opts[4],opts[5],opts[6],opts[7]+\'.\'+i+\'.children.\'+x+\'.children\']"')
.replace(/:childs\s*=\s*"n2.children"\s*:opts="opts"/, ':childs="n2.children" :opts="[opts[0],opts[1],opts[2],opts[3],opts[4],opts[5],opts[6],opts[7]+\'.\'+i+\'.children.\'+j+\'.children\']"')
.replace(/:childs\s*=\s*"tr.children"\s*:opts="opts"/, ':childs="tr.children" :opts="[opts[0],opts[1],opts[2],opts[3],opts[4],opts[5],opts[6],opts[7]+\'.\'+i+\'.children.\'+x+\'.children.\'+y+\'.children\']"')
.replace(/:childs\s*=\s*"td.children"\s*:opts="opts"/, ':childs="td.children" :opts="[opts[0],opts[1],opts[2],opts[3],opts[4],opts[5],opts[6],opts[7]+\'.\'+i+\'.children.\'+x+\'.children.\'+y+\'.children.\'+z+\'.children\']"')
.replace(/opts\s*=\s*"opts"/g, 'opts="[opts[0],opts[1],opts[2],opts[3],opts[4],opts[5],opts[6],opts[7]+\'.\'+i+\'.children\']"')
// 不使用 rich-text
.replace(/!n.c/g, '!opts[5]&&!n.c').replace('&&n.c', '&&(n.c||opts[5])')
// 修改普通标签
.replace(/<view\s+:id(.+?)style="/, '<view @tap="nodeTap" :id$1style="(ctrl.root&&opts[5]!==\'simple\'?\'border:1px solid black;padding:5px;display:block;\':\'\')+')
// 修改文本块
.replace(/<!--\s*文本\s*-->[\s\S]+?<!--\s*链接\s*-->/,
`<!-- 文本 -->
<text v-else-if="n.type==='text'&&!ctrl['e'+i]" :data-i="i" :user-select="opts[4]" :decode="!opts[5]" @tap="editStart">{{n.text}}
<text v-if="!n.text" style="color:gray">{{opts[6]||'请输入'}}</text>
</text>
<text v-else-if="n.type==='text'&&ctrl['e'+i]===1" :data-i="i" style="border:1px dashed black;min-width:50px;width:auto;padding:5px;display:block" @tap.stop="editStart">{{n.text}}
<text v-if="!n.text" style="color:gray">{{opts[6]||'请输入'}}</text>
</text>
<textarea v-else-if="n.type==='text'" :style="opts[5]==='simple'?'':'border:1px dashed black;'+'min-width:50px;width:auto;padding:5px'" auto-height maxlength="-1" :focus="ctrl['e'+i]===3" :value="n.text" :data-i="i" @input="editInput" @blur="editEnd" />
<text v-else-if="n.name==='br'">\\n</text>
<!-- 链接 -->`)
// 修改图片
.replace(/<image(.+?)id="n.attrs.id/, '<image$1id="n.attrs.id||(\'n\'+i)')
.replace('height:1px', "height:'+(ctrl['h'+i]||1)+'px")
.replace(/:style\s*=\s*"\(ctrl\[i\]/g, ':style="(ctrl[\'e\'+i]&&opts[5]!==\'simple\'?\'border:1px dashed black;padding:3px;\':\'\')+(ctrl[i]')
.replace(/show-menu-by-longpress\s*=\s*"(\S+?)"\s*:image-menu-prevent\s*=\s*"(\S+?)"/, 'show-menu-by-longpress="!opts[5]&&$1" :image-menu-prevent="opts[5]||$2"')
// 修改音视频
.replace('v-else-if="n.html"', 'v-else-if="n.html" :data-i="i" @tap="mediaTap"')
.replace('<video', '<video :show-center-play-btn="!opts[5]" @tap="mediaTap"')
.replace('<audio ', '<audio @tap="mediaTap" ')
.replace('<my-audio ', '<my-audio @onClick="mediaTap($event, i)" ')
.replace('card ', 'card @click="mediaTap($event, i)" ')
.replace('<script>',
`<script>
import Parser from '../parser'
function getTop(e) {
let top
// #ifdef H5 && VUE3
top = e.pageY
// #endif
// #ifdef (H5 && VUE2) || APP-PLUS
top = e.touches[0].pageY
// #endif
// #ifdef MP-ALIPAY
top = e.detail.pageY
// #endif
// #ifndef H5 || MP-ALIPAY || APP-PLUS
top = e.detail.y
// #endif
if (top - e.currentTarget.offsetTop < 150 || top < 600) {
top = e.currentTarget.offsetTop
}
if (top < 30) {
top += 70
}
return top - 30
}`)
// 周期处理
.replace(/beforeDestroy\s*\(\)\s*{/, `beforeDestroy () {
if (this.root && this.root._edit === this) {
this.root._edit = undefined
}`)
// 记录图片宽度
.replace(/imgLoad\s*\(e\)\s*{/, `imgLoad(e) {
// #ifdef MP-WEIXIN || MP-QQ
if (this.opts[5])
this.$nextTick(() => {
const id = this.childs[i].attrs.id || ('n' + i)
uni.createSelectorQuery().in(this).select('#' + id).boundingClientRect().exec(res => {
this.$set(this.ctrl, 'h'+i, res[0].height)
})
})
// #endif`)
.replace(/if\s*\(!this.childs\[i\].w\)\s*{[\s\S]+?}/,
`if (!this.childs[i].w) {
this.$set(this.ctrl, i, e.detail.width)
if (this.opts[5]) {
const path = this.opts[7] + '.' + i + '.attrs.'
if (e.detail.width < 150)
this.root._setData(path + 'ignore', 'T')
this.root._setData(path + 'width', e.detail.width.toString())
}
}`)
// 处理图片长按
.replace(/imgLongTap\s*\(\)\s*{/, `imgLongTap() {
if (this.opts[5]) return`)
// 处理图片点击
.replace(/imgTap\s*\(e\)\s*{([\s\S]+?)},\s*\/\*/,
`imgTap (e) {
if (!this.opts[5]) {$1} else {
const i = e.currentTarget.dataset.i
const node = this.childs[i]
const items = this.root._getItem(node)
const parser = new Parser(this.root)
this.root._edit = this
this.i = i
this.root._maskTap()
this.$set(this.ctrl, 'e' + i, 1)
this.root._mask.push(() => this.$set(this.ctrl, 'e' + i, 0))
this.root._tooltip({
top: getTop(e),
items,
success: tapIndex => {
if (items[tapIndex] === '换图') {
// 换图
this.root.getSrc('img', node.attrs.src || '').then(url => {
this.root._editVal(this.opts[7] + '.' + i + '.attrs.src', node.attrs.src, parser.getUrl(url instanceof Array ? url[0] : url), true)
}).catch(() => { })
} else if (items[tapIndex] === '宽度') {
// 更改宽度
const style = node.attrs.style || ''
let value = style.match(/max-width:([0-9]+)%/)
if (value) {
value = parseInt(value[1])
} else {
value = 100
}
this.root._slider({
min: 0,
max: 100,
value,
top: getTop(e),
changing: val => {
// 变化超过 5% 更新时视图
if (Math.abs(val - value) > 5) {
this.changeStyle('max-width', i, val + '%', value + '%')
value = val
}
},
change: val => {
if (val !== value) {
this.changeStyle('max-width', i, val + '%', value + '%')
value = val
}
this.root._editVal(this.opts[7] + '.' + i + '.attrs.style', style, this.childs[i].attrs.style)
}
})
} else if (items[tapIndex] === '超链接') {
// 将图片设置为链接
this.root.getSrc('link', node.a ? node.a.href : '').then(url => {
// 如果有 a 标签则替换 href
if (node.a) {
this.root._editVal(this.opts[7] + '.' + i + '.a.href', node.a.href, parser.getUrl(url), true)
} else {
const link = {
name: 'a',
attrs: {
href: parser.getUrl(url)
},
children: [node]
}
node.a = link.attrs
this.root._editVal(this.opts[7] + '.' + i, node, link, true)
}
wx.showToast({
title: '成功'
})
}).catch(() => { })
} else if (items[tapIndex] === '预览图') {
// 设置预览图链接
this.root.getSrc('img', node.attrs['original-src'] || '').then(url => {
this.root._editVal(this.opts[7] + '.' + i + '.attrs.original-src', node.attrs['original-src'], parser.getUrl(url instanceof Array ? url[0] : url), true)
uni.showToast({
title: '成功'
})
}).catch(() => { })
} else if (items[tapIndex] === '删除') {
this.remove(i)
} else {
// 禁用 / 启用预览
this.root._setData(this.opts[7] + '.' + i + '.attrs.ignore', !node.attrs.ignore)
uni.showToast({
title: '成功'
})
}
}
})
this.root._lock = true
setTimeout(() => {
this.root._lock = false
}, 50)
}
},
/*`)
// 处理链接点击
.replace(/linkTap\s*\(e\)\s*{([\s\S]+?)},\s*\/\*/,
`linkTap (e) {
if (!this.opts[5]) {$1} else {
const i = e.currentTarget.dataset.i
const node = this.childs[i]
const items = this.root._getItem(node)
this.root._tooltip({
top: getTop(e),
items,
success: tapIndex => {
if (items[tapIndex] === '更换链接') {
this.root.getSrc('link', node.attrs.href).then(url => {
this.root._editVal(this.opts[7] + '.' + i + '.attrs.href', node.attrs.href, url, true)
uni.showToast({
title: '成功'
})
}).catch(() => { })
} else {
this.remove(i)
}
}
})
}
},
/*`)
}
file.contents = Buffer.from(content)
}
}
}
+553
View File
@@ -0,0 +1,553 @@
/**
* @fileoverview editable 插件
*/
const config = require('./config')
const Parser = require('../parser')
function Editable (vm) {
this.vm = vm
this.editHistory = [] // 历史记录
this.editI = -1 // 历史记录指针
vm._mask = [] // 蒙版被点击时进行的操作
vm._setData = function (path, val) {
const paths = path.split('.')
let target = vm
for (let i = 0; i < paths.length - 1; i++) {
target = target[paths[i]]
}
vm.$set(target, paths.pop(), val)
}
/**
* @description 移动历史记录指针
* @param {Number} num 移动距离
*/
const move = num => {
setTimeout(() => {
const item = this.editHistory[this.editI + num]
if (item) {
this.editI += num
vm._setData(item.key, item.value)
}
}, 200)
}
vm.undo = () => move(-1) // 撤销
vm.redo = () => move(1) // 重做
/**
* @description 更新记录
* @param {String} path 更新内容路径
* @param {*} oldVal 旧值
* @param {*} newVal 新值
* @param {Boolean} set 是否更新到视图
* @private
*/
vm._editVal = (path, oldVal, newVal, set) => {
// 当前指针后的内容去除
while (this.editI < this.editHistory.length - 1) {
this.editHistory.pop()
}
// 最多存储 30 条操作记录
while (this.editHistory.length > 30) {
this.editHistory.pop()
this.editI--
}
const last = this.editHistory[this.editHistory.length - 1]
if (!last || last.key !== path) {
if (last) {
// 去掉上一次的新值
this.editHistory.pop()
this.editI--
}
// 存入这一次的旧值
this.editHistory.push({
key: path,
value: oldVal
})
this.editI++
}
// 存入本次的新值
this.editHistory.push({
key: path,
value: newVal
})
this.editI++
// 更新到视图
if (set) {
vm._setData(path, newVal)
}
}
/**
* @description 获取菜单项
* @private
*/
vm._getItem = function (node, up, down) {
let items
let i
if (node === 'color') {
return config.color
}
if (node.name === 'img') {
items = config.img.slice(0)
if (!vm.getSrc) {
i = items.indexOf('换图')
if (i !== -1) {
items.splice(i, 1)
}
i = items.indexOf('超链接')
if (i !== -1) {
items.splice(i, 1)
}
i = items.indexOf('预览图')
if (i !== -1) {
items.splice(i, 1)
}
}
i = items.indexOf('禁用预览')
if (i !== -1 && node.attrs.ignore) {
items[i] = '启用预览'
}
} else if (node.name === 'a') {
items = config.link.slice(0)
if (!vm.getSrc) {
i = items.indexOf('更换链接')
if (i !== -1) {
items.splice(i, 1)
}
}
} else if (node.name === 'video' || node.name === 'audio') {
items = config.media.slice(0)
i = items.indexOf('封面')
if (!vm.getSrc && i !== -1) {
items.splice(i, 1)
}
i = items.indexOf('循环')
if (node.attrs.loop && i !== -1) {
items[i] = '不循环'
}
i = items.indexOf('自动播放')
if (node.attrs.autoplay && i !== -1) {
items[i] = '不自动播放'
}
} else if (node.name === 'card') {
items = config.card.slice(0)
} else {
items = config.node.slice(0)
}
if (!up) {
i = items.indexOf('上移')
if (i !== -1) {
items.splice(i, 1)
}
}
if (!down) {
i = items.indexOf('下移')
if (i !== -1) {
items.splice(i, 1)
}
}
return items
}
/**
* @description 显示 tooltip
* @param {object} obj
* @private
*/
vm._tooltip = function (obj) {
vm.$set(vm, 'tooltip', {
top: obj.top,
items: obj.items
})
vm._tooltipcb = obj.success
}
/**
* @description 显示滚动条
* @param {object} obj
* @private
*/
vm._slider = function (obj) {
vm.$set(vm, 'slider', {
min: obj.min,
max: obj.max,
value: obj.value,
top: obj.top
})
vm._slideringcb = obj.changing
vm._slidercb = obj.change
}
/**
* @description 显示颜色选择
* @param {object} obj
* @private
*/
vm._color = function (obj) {
vm.$set(vm, 'color', {
items: obj.items,
top: obj.top
})
vm._colorcb = obj.success
}
/**
* @description 点击蒙版
* @private
*/
vm._maskTap = function () {
// 隐藏所有悬浮窗
while (vm._mask.length) {
(vm._mask.pop())()
}
if (vm.tooltip) {
vm.$set(vm, 'tooltip', null)
}
if (vm.slider) {
vm.$set(vm, 'slider', null)
}
if (vm.color) {
vm.$set(vm, 'color', null)
}
}
/**
* @description 插入节点
* @param {Object} node
*/
function insert (node) {
if (vm._edit) {
vm._edit.insert(node)
} else {
const nodes = vm.nodes.slice(0)
nodes.push(node)
vm._editVal('nodes', vm.nodes, nodes, true)
}
}
/**
* @description 在光标处插入指定 html 内容
* @param {String} html 内容
*/
vm.insertHtml = html => {
this.inserting = true
const arr = new Parser(vm).parse(html)
this.inserting = undefined
for (let i = 0; i < arr.length; i++) {
insert(arr[i])
}
}
/**
* @description 在光标处插入图片
*/
vm.insertImg = function () {
vm.getSrc && vm.getSrc('img').then(src => {
if (typeof src === 'string') {
src = [src]
}
const parser = new Parser(vm)
for (let i = 0; i < src.length; i++) {
insert({
name: 'img',
attrs: {
src: parser.getUrl(src[i])
}
})
}
}).catch(() => { })
}
/**
* @description 在光标处插入一个链接
*/
vm.insertLink = function () {
vm.getSrc && vm.getSrc('link').then(url => {
insert({
name: 'a',
attrs: {
href: url
},
children: [{
type: 'text',
text: url
}]
})
}).catch(() => { })
}
/**
* @description 在光标处插入一个表格
* @param {Number} rows 行数
* @param {Number} cols 列数
*/
vm.insertTable = function (rows, cols) {
const table = {
name: 'table',
attrs: {
style: 'display:table;width:100%;margin:10px 0;text-align:center;border-spacing:0;border-collapse:collapse;border:1px solid gray'
},
children: []
}
for (let i = 0; i < rows; i++) {
const tr = {
name: 'tr',
attrs: {},
children: []
}
for (let j = 0; j < cols; j++) {
tr.children.push({
name: 'td',
attrs: {
style: 'padding:2px;border:1px solid gray'
},
children: [{
type: 'text',
text: ''
}]
})
}
table.children.push(tr)
}
insert(table)
}
/**
* @description 插入视频/音频
* @param {Object} node
*/
function insertMedia (node) {
if (typeof node.src === 'string') {
node.src = [node.src]
}
const parser = new Parser(vm)
// 拼接主域名
for (let i = 0; i < node.src.length; i++) {
node.src[i] = parser.getUrl(node.src[i])
}
insert({
name: 'div',
attrs: {
style: 'text-align:center'
},
children: [node]
})
}
/**
* @description 在光标处插入一个视频
*/
vm.insertVideo = function () {
vm.getSrc && vm.getSrc('video').then(src => {
insertMedia({
name: 'video',
attrs: {
controls: 'T'
},
children: [],
src,
// #ifdef APP-PLUS
html: `<video src="${src}" style="width:100%;height:100%"></video>`
// #endif
})
}).catch(() => { })
}
/**
* @description 在光标处插入一个音频
*/
vm.insertAudio = function () {
vm.getSrc && vm.getSrc('audio').then(attrs => {
let src
if (attrs.src) {
src = attrs.src
attrs.src = undefined
} else {
src = attrs
attrs = {}
}
attrs.controls = 'T'
insertMedia({
name: 'audio',
attrs,
children: [],
src
})
}).catch(() => { })
}
/**
* @description 在光标处插入一段文本
*/
vm.insertText = function () {
insert({
name: 'p',
attrs: {},
children: [{
type: 'text',
text: ''
}]
})
}
/**
* @description 清空内容
*/
vm.clear = function () {
vm._maskTap()
vm._edit = undefined
vm.$set(vm, 'nodes', [{
name: 'p',
attrs: {},
children: [{
type: 'text',
text: ''
}]
}])
}
/**
* @description 获取编辑后的 html
*/
vm.getContent = function () {
let html = '';
// 递归遍历获取
(function traversal (nodes, table) {
for (let i = 0; i < nodes.length; i++) {
let item = nodes[i]
if (item.type === 'text') {
html += item.text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, '<br>').replace(/\xa0/g, '&nbsp;') // 编码实体
} else {
if (item.name === 'img') {
item.attrs.i = ''
// 还原被转换的 svg
if ((item.attrs.src || '').includes('data:image/svg+xml;utf8,')) {
html += item.attrs.src.substr(24).replace(/%23/g, '#').replace('<svg', '<svg style="' + (item.attrs.style || '') + '"')
continue
}
} else if (item.name === 'video' || item.name === 'audio') {
// 还原 video 和 audio 的 source
item = JSON.parse(JSON.stringify(item))
if (item.src.length > 1) {
item.children = []
for (let j = 0; j < item.src.length; j++) {
item.children.push({
name: 'source',
attrs: {
src: item.src[j]
}
})
}
} else {
item.attrs.src = item.src[0]
}
} else if (item.name === 'div' && (item.attrs.style || '').includes('overflow:auto') && (item.children[0] || {}).name === 'table') {
// 还原滚动层
item = item.children[0]
}
// 还原 table
if (item.name === 'table') {
item = JSON.parse(JSON.stringify(item))
table = item.attrs
if ((item.attrs.style || '').includes('display:grid')) {
item.attrs.style = item.attrs.style.split('display:grid')[0]
const children = [{
name: 'tr',
attrs: {},
children: []
}]
for (let j = 0; j < item.children.length; j++) {
item.children[j].attrs.style = item.children[j].attrs.style.replace(/grid-[^;]+;*/g, '')
if (item.children[j].r !== children.length) {
children.push({
name: 'tr',
attrs: {},
children: [item.children[j]]
})
} else {
children[children.length - 1].children.push(item.children[j])
}
}
item.children = children
}
}
html += '<' + item.name
for (const attr in item.attrs) {
let val = item.attrs[attr]
if (!val) continue
if (val === 'T' || val === true) {
// bool 型省略值
html += ' ' + attr
continue
} else if (item.name[0] === 't' && attr === 'style' && table) {
// 取消为了显示 table 添加的 style
val = val.replace(/;*display:table[^;]*/, '')
if (table.border) {
val = val.replace(/border[^;]+;*/g, $ => $.includes('collapse') ? $ : '')
}
if (table.cellpadding) {
val = val.replace(/padding[^;]+;*/g, '')
}
if (!val) continue
}
html += ' ' + attr + '="' + val.replace(/"/g, '&quot;') + '"'
}
html += '>'
if (item.children) {
traversal(item.children, table)
html += '</' + item.name + '>'
}
}
}
})(vm.nodes)
// 其他插件处理
for (let i = vm.plugins.length; i--;) {
if (vm.plugins[i].onGetContent) {
html = vm.plugins[i].onGetContent(html) || html
}
}
return html
}
}
Editable.prototype.onUpdate = function (content, config) {
if (this.vm.editable) {
this.vm._maskTap()
config.entities.amp = '&'
if (!this.inserting) {
this.vm._edit = undefined
if (!content) {
setTimeout(() => {
this.vm.$set(this.vm, 'nodes', [{
name: 'p',
attrs: {},
children: [{
type: 'text',
text: ''
}]
}])
}, 0)
}
}
}
}
Editable.prototype.onParse = function (node) {
// 空白单元格可编辑
if (this.vm.editable && (node.name === 'td' || node.name === 'th') && !this.vm.getText(node.children)) {
node.children.push({
type: 'text',
text: ''
})
}
}
module.exports = Editable
+15
View File
@@ -0,0 +1,15 @@
# emoji
功能:解析 *emoji*
大小:*≈3KB*
支持平台:
| 微信小程序 | QQ 小程序 | 百度小程序 | 支付宝小程序 | 头条小程序 | uni-app |
|:---:|:---:|:---:|:---:|:---:|:---:|
| √ | √ | √ | √ | √ | √ |
说明:
将形如 *[笑脸]* 的文本替换为 *emoji* 字符 😄
匹配模式可以通过修改 *reg* 变量实现
默认配置了 *177* 个常用的 *emoji* 小表情,可以自行按照需要修改 *data* 变量
?> 与 *editable* 插件共用时,导出编辑好的 *html* 内容,会将 *emoji* 字符编码为文本形式,便于存储
+203
View File
@@ -0,0 +1,203 @@
/**
* @fileoverview emoji 插件
*/
const reg = /\[(\S+?)\]/g
const data = {
笑脸: '😄',
生病: '😷',
破涕为笑: '😂',
吐舌: '😝',
脸红: '😳',
恐惧: '😱',
失望: '😔',
无语: '😒',
眨眼: '😉',
酷: '😎',
哭: '😭',
痴迷: '😍',
吻: '😘',
思考: '🤔',
困惑: '😕',
颠倒: '🙃',
钱: '🤑',
惊讶: '😲',
白眼: '🙄',
叹气: '😤',
睡觉: '😴',
书呆子: '🤓',
愤怒: '😡',
面无表情: '😑',
张嘴: '😮',
量体温: '🤒',
呕吐: '🤮',
光环: '😇',
幽灵: '👻',
外星人: '👽',
机器人: '🤖',
捂眼镜: '🙈',
捂耳朵: '🙉',
捂嘴: '🙊',
婴儿: '👶',
男孩: '👦',
女孩: '👧',
男人: '👨',
女人: '👩',
老人: '👴',
老妇人: '👵',
警察: '👮',
王子: '🤴',
公主: '🤴',
举手: '🙋',
跑步: '🏃',
家庭: '👪',
眼睛: '👀',
鼻子: '👃',
耳朵: '👂',
舌头: '👅',
嘴: '👄',
心: '❤️',
心碎: '💔',
雪人: '☃️',
情书: '💌',
大便: '💩',
闹钟: '⏰',
眼镜: '👓',
雨伞: '☂️',
音乐: '🎵',
话筒: '🎤',
游戏机: '🎮',
喇叭: '📢',
耳机: '🎧',
礼物: '🎁',
电话: '📞',
电脑: '💻',
打印机: '🖨️',
手电筒: '🔦',
灯泡: '💡',
书本: '📖',
信封: '✉️',
药丸: '💊',
口红: '💄',
手机: '📱',
相机: '📷',
电视: '📺',
中: '🀄',
垃圾桶: '🚮',
厕所: '🚾',
感叹号: '❗',
禁: '🈲',
可: '🉑',
彩虹: '🌈',
旋风: '🌀',
雷电: '⚡',
雪花: '❄️',
星星: '⭐',
水滴: '💧',
玫瑰: '🌹',
加油: '💪',
左: '👈',
右: '👉',
上: '👆',
下: '👇',
手掌: '🖐️',
好的: '👌',
好: '👍',
差: '👎',
胜利: '✌',
拳头: '👊',
挥手: '👋',
鼓掌: '👏',
猴子: '🐒',
狗: '🐶',
狼: '🐺',
猫: '🐱',
老虎: '🐯',
马: '🐎',
独角兽: '🦄',
斑马: '🦓',
鹿: '🦌',
牛: '🐮',
猪: '🐷',
羊: '🐏',
长颈鹿: '🦒',
大象: '🐘',
老鼠: '🐭',
蝙蝠: '🦇',
刺猬: '🦔',
熊猫: '🐼',
鸽子: '🕊️',
鸭子: '🦆',
兔子: '🐇',
老鹰: '🦅',
青蛙: '🐸',
蛇: '🐍',
龙: '🐉',
鲸鱼: '🐳',
海豚: '🐬',
足球: '⚽',
棒球: '⚾',
篮球: '🏀',
排球: '🏐',
橄榄球: '🏉',
网球: '🎾',
骰子: '🎲',
鸡腿: '🍗',
蛋糕: '🎂',
啤酒: '🍺',
饺子: '🥟',
汉堡: '🍔',
薯条: '🍟',
意大利面: '🍝',
干杯: '🥂',
筷子: '🥢',
糖果: '🍬',
奶瓶: '🍼',
爆米花: '🍿',
邮局: '🏤',
医院: '🏥',
银行: '🏦',
酒店: '🏨',
学校: '🏫',
城堡: '🏰',
火车: '🚂',
高铁: '🚄',
地铁: '🚇',
公交: '🚌',
救护车: '🚑',
消防车: '🚒',
警车: '🚓',
出租车: '🚕',
汽车: '🚗',
货车: '🚛',
自行车: '🚲',
摩托: '🛵',
红绿灯: '🚥',
帆船: '⛵',
游轮: '🛳️',
轮船: '⛴️',
飞机: '✈️',
直升机: '🚁',
缆车: '🚠',
警告: '⚠️',
禁止: '⛔'
}
function Emoji () {
}
Emoji.prototype.onUpdate = function (content) {
return content.replace(reg, ($, $1) => {
if (data[$1]) return data[$1]
return $
})
}
Emoji.prototype.onGetContent = function (content) {
for (const item in data) {
content = content.replace(new RegExp(data[item], 'g'), '[' + item + ']')
}
return content
}
module.exports = Emoji
+26
View File
@@ -0,0 +1,26 @@
# highlight
功能:代码块高亮显示
支持平台:
| 微信小程序 | QQ 小程序 | 百度小程序 | 支付宝小程序 | 头条小程序 | uni-app |
|:---:|:---:|:---:|:---:|:---:|:---:|
| √ | √ | √ | √ | √ | √ |
说明:
大小:*≈16KB*
编辑 *plugins/highlight/config.js* 顶部的选项,可以选择是否需要以下功能:
- *copyByLongPress* 是否需要长按代码块时显示复制代码内容菜单(*uni-app nvue* 暂不支持)
- *showLanguageName* 是否在代码块右上角显示语言的名称
- *showLineNumber* 是否在左侧显示行号
> 修改该配置后需要重新生成组件包,在构建后的组件包中修改配置无法生效
引入本插件后,*html* 中符合以下格式的 *pre* 将被高亮处理:
```html
<!-- pre 中内含一个 code,并在 pre 或 code 的 class 中设置 language- -->
<pre><code class="language-css">p { color: red }</code></pre>
```
> 与 *editable* 插件共用时,编辑状态下,不会进行高亮,可以直接修改代码文本
> 本插件的高亮功能依赖于 [prismjs](https://prismjs.com/),默认配置中仅支持 *html*、*css*、*c-like*、*javascript* 语言和 *Tomorrow Night* 主题,如果需要更多语言或更换主题请前往 [官网](https://prismjs.com/download.html) 下载对应的 *prism.min.js* 和 *prism.css* 并替换 *plugins/highlight/* 目录下的文件
+5
View File
@@ -0,0 +1,5 @@
module.exports = {
copyByLongPress: false, // 是否需要长按代码块时显示复制代码内容菜单
showLanguageName: false, // 是否在代码块右上角显示语言的名称
showLineNumber: false // 是否显示行号
}
+96
View File
@@ -0,0 +1,96 @@
/**
* @fileoverview highlight 插件
* Include prismjs (https://prismjs.com)
*/
const prism = require('./prism.min')
const config = require('./config')
const Parser = require('../parser')
function Highlight (vm) {
this.vm = vm
}
Highlight.prototype.onParse = function (node, vm) {
if (node.name === 'pre') {
if (vm.options.editable) {
node.attrs.class = (node.attrs.class || '') + ' hl-pre'
return
}
let i
for (i = node.children.length; i--;) {
if (node.children[i].name === 'code') break
}
if (i === -1) return
const code = node.children[i]
let className = code.attrs.class + ' ' + node.attrs.class
i = className.indexOf('language-')
if (i === -1) {
i = className.indexOf('lang-')
if (i === -1) {
className = 'language-text'
i = 9
} else {
i += 5
}
} else {
i += 9
}
let j
for (j = i; j < className.length; j++) {
if (className[j] === ' ') break
}
const lang = className.substring(i, j)
if (code.children.length) {
const text = this.vm.getText(code.children).replace(/&amp;/g, '&')
if (!text) return
if (node.c) {
node.c = undefined
}
if (prism.languages[lang]) {
code.children = (new Parser(this.vm).parse(
// 加一层 pre 保留空白符
'<pre>' + prism.highlight(text, prism.languages[lang], lang).replace(/token /g, 'hl-') + '</pre>'))[0].children
}
node.attrs.class = 'hl-pre'
code.attrs.class = 'hl-code'
if (config.showLanguageName) {
node.children.push({
name: 'div',
attrs: {
class: 'hl-language',
style: 'user-select:none'
},
children: [{
type: 'text',
text: lang
}]
})
}
if (config.copyByLongPress) {
node.attrs.style += (node.attrs.style || '') + ';user-select:none'
node.attrs['data-content'] = text
vm.expose()
}
if (config.showLineNumber) {
const line = text.split('\n').length; const children = []
for (let k = line; k--;) {
children.push({
name: 'span',
attrs: {
class: 'span'
}
})
}
node.children.push({
name: 'span',
attrs: {
class: 'line-numbers-rows'
},
children
})
}
}
}
}
module.exports = Highlight
@@ -0,0 +1,88 @@
const config = require('../config')
const build = {
import: 'prism.css',
handler (file) {
if (file.path.includes('prism.css')) {
// 将标签名选择器和属性选择器转为 class 选择器(组件内仅支持 class 选择器)
file.contents = Buffer.from(file.contents.toString().replace(/pre([[)])/g, '.hl-pre$1').replace(/code/g, '.hl-code').replace(/\[class\*="?language-"?\]/g, '').replace(/:not[^,}]+[,}]*/g, '').replace(/\.token\./g, '.hl-'))
}
}
}
if (config.showLanguageName || config.showLineNumber) {
// pre 内部的 code 进行滚动,避免行号和语言名称跟随滚动
build.style = `.hl-pre {
position: relative;
}
.hl-code {
overflow: auto;
display: block;
}`
}
if (config.copyByLongPress) {
build.template = '<rich-text wx:if="{{n.attrs[\'data-content\']}}" nodes="{{[n]}}" data-content="{{n.attrs[\'data-content\']}}" data-lang="{{n.attrs[\'data-lang\']}}" bindlongpress="copyCode" />'
build.methods = {
copyCode (e) {
wx.showActionSheet({
itemList: ['复制代码'],
success: () =>
wx.setClipboardData({
data: e.currentTarget.dataset.content
})
})
}
}
}
if (config.showLanguageName) {
build.style = (build.style || '') +
`.hl-language {
font-size: 12px;
font-weight: 600;
position: absolute;
right: 8px;
text-align: right;
top: 3px;
}
.hl-pre {
padding-top: 1.5em;
}`
}
if (config.showLineNumber) {
build.style = (build.style || '') +
`.hl-pre {
font-size: 14px;
padding-left: 3.8em;
counter-reset: linenumber;
}
.line-numbers-rows {
position: absolute;
pointer-events: none;
top: ${config.showLanguageName ? 1.5 : 1}em;
font-size: 100%;
left: 0;
width: 3em; /* works for line-numbers below 1000 lines */
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: 0.8em;
text-align: right;
}`
}
module.exports = build
+125
View File
@@ -0,0 +1,125 @@
/* PrismJS 1.22.0
https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript */
/**
* prism.js tomorrow night eighties for JavaScript, CoffeeScript, CSS and HTML
* Based on https://github.com/chriskempson/tomorrow-theme
* @author Rose Pritchard
*/
code[class*="language-"],
pre[class*="language-"] {
color: #ccc;
background: none;
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 blocks */
pre[class*="language-"] {
padding: 1em;
margin: .5em 0;
overflow: auto;
}
:not(pre) > code[class*="language-"],
pre[class*="language-"] {
background: #2d2d2d;
}
/* Inline code */
:not(pre) > code[class*="language-"] {
padding: .1em;
border-radius: .3em;
white-space: normal;
}
.token.comment,
.token.block-comment,
.token.prolog,
.token.doctype,
.token.cdata {
color: #999;
}
.token.punctuation {
color: #ccc;
}
.token.tag,
.token.attr-name,
.token.namespace,
.token.deleted {
color: #e2777a;
}
.token.function-name {
color: #6196cc;
}
.token.boolean,
.token.number,
.token.function {
color: #f08d49;
}
.token.property,
.token.class-name,
.token.constant,
.token.symbol {
color: #f8c555;
}
.token.selector,
.token.important,
.token.atrule,
.token.keyword,
.token.builtin {
color: #cc99cd;
}
.token.string,
.token.char,
.token.attr-value,
.token.regex,
.token.variable {
color: #7ec699;
}
.token.operator,
.token.entity,
.token.url {
color: #67cdcc;
}
.token.important,
.token.bold {
font-weight: bold;
}
.token.italic {
font-style: italic;
}
.token.entity {
cursor: help;
}
.token.inserted {
color: green;
}
+7
View File
@@ -0,0 +1,7 @@
/*! PrismJS 1.22.0
https://prismjs.com/download.html#themes=prism-tomorrow&languages=markup+css+clike+javascript */
var _self="undefined"!=typeof window?window:"undefined"!=typeof WorkerGlobalScope&&self instanceof WorkerGlobalScope?self:{},Prism=function(u){var c=/\blang(?:uage)?-([\w-]+)\b/i,n=0,M={manual:u.Prism&&u.Prism.manual,disableWorkerMessageHandler:u.Prism&&u.Prism.disableWorkerMessageHandler,util:{encode:function e(n){return n instanceof W?new W(n.type,e(n.content),n.alias):Array.isArray(n)?n.map(e):n.replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/\u00a0/g," ")},type:function(e){return Object.prototype.toString.call(e).slice(8,-1)},objId:function(e){return e.__id||Object.defineProperty(e,"__id",{value:++n}),e.__id},clone:function t(e,r){var a,n;switch(r=r||{},M.util.type(e)){case"Object":if(n=M.util.objId(e),r[n])return r[n];for(var i in a={},r[n]=a,e)e.hasOwnProperty(i)&&(a[i]=t(e[i],r));return a;case"Array":return n=M.util.objId(e),r[n]?r[n]:(a=[],r[n]=a,e.forEach(function(e,n){a[n]=t(e,r)}),a);default:return e}},getLanguage:function(e){for(;e&&!c.test(e.className);)e=e.parentElement;return e?(e.className.match(c)||[,"none"])[1].toLowerCase():"none"},currentScript:function(){if("undefined"==typeof document)return null;if("currentScript"in document)return document.currentScript;try{throw new Error}catch(e){var n=(/at [^(\r\n]*\((.*):.+:.+\)$/i.exec(e.stack)||[])[1];if(n){var t=document.getElementsByTagName("script");for(var r in t)if(t[r].src==n)return t[r]}return null}},isActive:function(e,n,t){for(var r="no-"+n;e;){var a=e.classList;if(a.contains(n))return!0;if(a.contains(r))return!1;e=e.parentElement}return!!t}},languages:{extend:function(e,n){var t=M.util.clone(M.languages[e]);for(var r in n)t[r]=n[r];return t},insertBefore:function(t,e,n,r){var a=(r=r||M.languages)[t],i={};for(var l in a)if(a.hasOwnProperty(l)){if(l==e)for(var o in n)n.hasOwnProperty(o)&&(i[o]=n[o]);n.hasOwnProperty(l)||(i[l]=a[l])}var s=r[t];return r[t]=i,M.languages.DFS(M.languages,function(e,n){n===s&&e!=t&&(this[e]=i)}),i},DFS:function e(n,t,r,a){a=a||{};var i=M.util.objId;for(var l in n)if(n.hasOwnProperty(l)){t.call(n,l,n[l],r||l);var o=n[l],s=M.util.type(o);"Object"!==s||a[i(o)]?"Array"!==s||a[i(o)]||(a[i(o)]=!0,e(o,t,l,a)):(a[i(o)]=!0,e(o,t,null,a))}}},plugins:{},highlightAll:function(e,n){M.highlightAllUnder(document,e,n)},highlightAllUnder:function(e,n,t){var r={callback:t,container:e,selector:'code[class*="language-"], [class*="language-"] code, code[class*="lang-"], [class*="lang-"] code'};M.hooks.run("before-highlightall",r),r.elements=Array.prototype.slice.apply(r.container.querySelectorAll(r.selector)),M.hooks.run("before-all-elements-highlight",r);for(var a,i=0;a=r.elements[i++];)M.highlightElement(a,!0===n,r.callback)},highlightElement:function(e,n,t){var r=M.util.getLanguage(e),a=M.languages[r];e.className=e.className.replace(c,"").replace(/\s+/g," ")+" language-"+r;var i=e.parentElement;i&&"pre"===i.nodeName.toLowerCase()&&(i.className=i.className.replace(c,"").replace(/\s+/g," ")+" language-"+r);var l={element:e,language:r,grammar:a,code:e.textContent};function o(e){l.highlightedCode=e,M.hooks.run("before-insert",l),l.element.innerHTML=l.highlightedCode,M.hooks.run("after-highlight",l),M.hooks.run("complete",l),t&&t.call(l.element)}if(M.hooks.run("before-sanity-check",l),!l.code)return M.hooks.run("complete",l),void(t&&t.call(l.element));if(M.hooks.run("before-highlight",l),l.grammar)if(n&&u.Worker){var s=new Worker(M.filename);s.onmessage=function(e){o(e.data)},s.postMessage(JSON.stringify({language:l.language,code:l.code,immediateClose:!0}))}else o(M.highlight(l.code,l.grammar,l.language));else o(M.util.encode(l.code))},highlight:function(e,n,t){var r={code:e,grammar:n,language:t};return M.hooks.run("before-tokenize",r),r.tokens=M.tokenize(r.code,r.grammar),M.hooks.run("after-tokenize",r),W.stringify(M.util.encode(r.tokens),r.language)},tokenize:function(e,n){var t=n.rest;if(t){for(var r in t)n[r]=t[r];delete n.rest}var a=new i;return I(a,a.head,e),function e(n,t,r,a,i,l){for(var o in r)if(r.hasOwnProperty(o)&&r[o]){var s=r[o];s=Array.isArray(s)?s:[s];for(var u=0;u<s.length;++u){if(l&&l.cause==o+","+u)return;var c=s[u],g=c.inside,f=!!c.lookbehind,h=!!c.greedy,d=0,v=c.alias;if(h&&!c.pattern.global){var p=c.pattern.toString().match(/[imsuy]*$/)[0];c.pattern=RegExp(c.pattern.source,p+"g")}for(var m=c.pattern||c,y=a.next,k=i;y!==t.tail&&!(l&&k>=l.reach);k+=y.value.length,y=y.next){var b=y.value;if(t.length>n.length)return;if(!(b instanceof W)){var x=1;if(h&&y!=t.tail.prev){m.lastIndex=k;var w=m.exec(n);if(!w)break;var A=w.index+(f&&w[1]?w[1].length:0),P=w.index+w[0].length,S=k;for(S+=y.value.length;S<=A;)y=y.next,S+=y.value.length;if(S-=y.value.length,k=S,y.value instanceof W)continue;for(var E=y;E!==t.tail&&(S<P||"string"==typeof E.value);E=E.next)x++,S+=E.value.length;x--,b=n.slice(k,S),w.index-=k}else{m.lastIndex=0;var w=m.exec(b)}if(w){f&&(d=w[1]?w[1].length:0);var A=w.index+d,O=w[0].slice(d),P=A+O.length,L=b.slice(0,A),N=b.slice(P),j=k+b.length;l&&j>l.reach&&(l.reach=j);var C=y.prev;L&&(C=I(t,C,L),k+=L.lengthLine truncated
Prism.languages.markup={comment:/<!--[\s\S]*?-->/,prolog:/<\?[\s\S]+?\?>/,doctype:{pattern:/<!DOCTYPE(?:[^>"'[\]]|"[^"]*"|'[^']*')+(?:\[(?:[^<"'\]]|"[^"]*"|'[^']*'|<(?!!--)|<!--(?:[^-]|-(?!->))*-->)*\]\s*)?>/i,greedy:!0,inside:{"internal-subset":{pattern:/(\[)[\s\S]+(?=\]>$)/,lookbehind:!0,greedy:!0,inside:null},string:{pattern:/"[^"]*"|'[^']*'/,greedy:!0},punctuation:/^<!|>$|[[\]]/,"doctype-tag":/^DOCTYPE/,name:/[^\s<>'"]+/}},cdata:/<!\[CDATA\[[\s\S]*?]]>/i,tag:{pattern:/<\/?(?!\d)[^\s>\/=$<%]+(?:\s(?:\s*[^\s>\/=]+(?:\s*=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+(?=[\s>]))|(?=[\s/>])))+)?\s*\/?>/,greedy:!0,inside:{tag:{pattern:/^<\/?[^\s>\/]+/,inside:{punctuation:/^<\/?/,namespace:/^[^\s>\/:]+:/}},"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}},punctuation:/\/?>/,"attr-name":{pattern:/[^\s>\/]+/,inside:{namespace:/^[^\s>\/:]+:/}}}},entity:[{pattern:/&[\da-z]{1,8};/i,alias:"named-entity"},/&#x?[\da-f]{1,8};/i]},Prism.languages.markup.tag.inside["attr-value"].inside.entity=Prism.languages.markup.entity,Prism.languages.markup.doctype.inside["internal-subset"].inside=Prism.languages.markup,Prism.hooks.add("wrap",function(a){"entity"===a.type&&(a.attributes.title=a.content.replace(/&amp;/,"&"))}),Object.defineProperty(Prism.languages.markup.tag,"addInlined",{value:function(a,e){var s={};s["language-"+e]={pattern:/(^<!\[CDATA\[)[\s\S]+?(?=\]\]>$)/i,lookbehind:!0,inside:Prism.languages[e]},s.cdata=/^<!\[CDATA\[|\]\]>$/i;var n={"included-cdata":{pattern:/<!\[CDATA\[[\s\S]*?\]\]>/i,inside:s}};n["language-"+e]={pattern:/[\s\S]+/,inside:Prism.languages[e]};var t={};t[a]={pattern:RegExp("(<__[^]*?>)(?:<!\\[CDATA\\[(?:[^\\]]|\\](?!\\]>))*\\]\\]>|(?!<!\\[CDATA\\[)[^])*?(?=</__>)".replace(/__/g,function(){return a}),"i"),lookbehind:!0,greedy:!0,inside:n},Prism.languages.insertBefore("markup","cdata",t)}}),Prism.languages.html=Prism.languages.markup,Prism.languages.mathml=Prism.languages.markup,Prism.languages.svg=Prism.languages.markup,Prism.languages.xml=Prism.languages.extend("markup",{}),Prism.languages.ssml=Prism.languages.xml,Prism.languages.atom=Prism.languages.xml,Prism.languages.rss=Prism.languages.xml;
!function(e){var t=/("|')(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/;e.languages.css={comment:/\/\*[\s\S]*?\*\//,atrule:{pattern:/@[\w-]+[\s\S]*?(?:;|(?=\s*\{))/,inside:{rule:/^@[\w-]+/,"selector-function-argument":{pattern:/(\bselector\s*\((?!\s*\))\s*)(?:[^()]|\((?:[^()]|\([^()]*\))*\))+?(?=\s*\))/,lookbehind:!0,alias:"selector"},keyword:{pattern:/(^|[^\w-])(?:and|not|only|or)(?![\w-])/,lookbehind:!0}}},url:{pattern:RegExp("\\burl\\((?:"+t.source+"|(?:[^\\\\\r\n()\"']|\\\\[^])*)\\)","i"),greedy:!0,inside:{function:/^url/i,punctuation:/^\(|\)$/,string:{pattern:RegExp("^"+t.source+"$"),alias:"url"}}},selector:RegExp("[^{}\\s](?:[^{};\"']|"+t.source+")*?(?=\\s*\\{)"),string:{pattern:t,greedy:!0},property:/[-_a-z\xA0-\uFFFF][-\w\xA0-\uFFFF]*(?=\s*:)/i,important:/!important\b/i,function:/[-a-z0-9]+(?=\()/i,punctuation:/[(){};:,]/},e.languages.css.atrule.inside.rest=e.languages.css;var s=e.languages.markup;s&&(s.tag.addInlined("style","css"),e.languages.insertBefore("inside","attr-value",{"style-attr":{pattern:/(^|["'\s])style\s*=\s*(?:"[^"]*"|'[^']*')/i,lookbehind:!0,inside:{"attr-value":{pattern:/=\s*(?:"[^"]*"|'[^']*'|[^\s'">=]+)/,inside:{style:{pattern:/(["'])[\s\S]+(?=["']$)/,lookbehind:!0,alias:"language-css",inside:e.languages.css},punctuation:[{pattern:/^=/,alias:"attr-equals"},/"|'/]}},"attr-name":/^style/i}}},s.tag))}(Prism);
Prism.languages.clike={comment:[{pattern:/(^|[^\\])\/\*[\s\S]*?(?:\*\/|$)/,lookbehind:!0},{pattern:/(^|[^\\:])\/\/.*/,lookbehind:!0,greedy:!0}],string:{pattern:/(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/,greedy:!0},"class-name":{pattern:/(\b(?:class|interface|extends|implements|trait|instanceof|new)\s+|\bcatch\s+\()[\w.\\]+/i,lookbehind:!0,inside:{punctuation:/[.\\]/}},keyword:/\b(?:if|else|while|do|for|return|in|instanceof|function|new|try|throw|catch|finally|null|break|continue)\b/,boolean:/\b(?:true|false)\b/,function:/\w+(?=\()/,number:/\b0x[\da-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:e[+-]?\d+)?/i,operator:/[<>]=?|[!=]=?=?|--?|\+\+?|&&?|\|\|?|[?*/~^%]/,punctuation:/[{}[\];(),.:]/};
Prism.languages.javascript=Prism.languages.extend("clike",{"class-name":[Prism.languages.clike["class-name"],{pattern:/(^|[^$\w\xA0-\uFFFF])[_$A-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\.(?:prototype|constructor))/,lookbehind:!0}],keyword:[{pattern:/((?:^|})\s*)(?:catch|finally)\b/,lookbehind:!0},{pattern:/(^|[^.]|\.\.\.\s*)\b(?:as|async(?=\s*(?:function\b|\(|[$\w\xA0-\uFFFF]|$))|await|break|case|class|const|continue|debugger|default|delete|do|else|enum|export|extends|for|from|function|(?:get|set)(?=\s*[\[$\w\xA0-\uFFFF])|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)\b/,lookbehind:!0}],number:/\b(?:(?:0[xX](?:[\dA-Fa-f](?:_[\dA-Fa-f])?)+|0[bB](?:[01](?:_[01])?)+|0[oO](?:[0-7](?:_[0-7])?)+)n?|(?:\d(?:_\d)?)+n|NaN|Infinity)\b|(?:\b(?:\d(?:_\d)?)+\.?(?:\d(?:_\d)?)*|\B\.(?:\d(?:_\d)?)+)(?:[Ee][+-]?(?:\d(?:_\d)?)+)?/,function:/#?[_$a-zA-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*(?:\.\s*(?:apply|bind|call)\s*)?\()/,operator:/--|\+\+|\*\*=?|=>|&&=?|\|\|=?|[!=]==|<<=?|>>>?=?|[-+*/%&|^!=<>]=?|\.{3}|\?\?=?|\?\.?|[~:]/}),Prism.languages.javascript["class-name"][0].pattern=/(\b(?:class|interface|extends|implements|instanceof|new)\s+)[\w.\\]+/,Prism.languages.insertBefore("javascript","keyword",{regex:{pattern:/((?:^|[^$\w\xA0-\uFFFF."'\])\s]|\b(?:return|yield))\s*)\/(?:\[(?:[^\]\\\r\n]|\\.)*]|\\.|[^/\\\[\r\n])+\/[gimyus]{0,6}(?=(?:\s|\/\*(?:[^*]|\*(?!\/))*\*\/)*(?:$|[\r\n,.;:})\]]|\/\/))/,lookbehind:!0,greedy:!0,inside:{"regex-source":{pattern:/^(\/)[\s\S]+(?=\/[a-z]*$)/,lookbehind:!0,alias:"language-regex",inside:Prism.languages.regex},"regex-flags":/[a-z]+$/,"regex-delimiter":/^\/|\/$/}},"function-variable":{pattern:/#?[_$a-zA-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*[=:]\s*(?:async\s*)?(?:\bfunction\b|(?:\((?:[^()]|\([^()]*\))*\)|[_$a-zA-Z\xA0-\uFFFF][$\w\xA0-\uFFFF]*)\s*=>))/,alias:"function"},parameter:[{pattern:/(function(?:\s+[_$A-Za-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*)?\s*\(\s*)(?!\s)(?:[^()]|\([^()]*\))+?(?=\s*\))/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/[_$a-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*(?=\s*=>)/i,inside:Prism.languages.javascript},{pattern:/(\(\s*)(?!\s)(?:[^()]|\([^()]*\))+?(?=\s*\)\s*=>)/,lookbehind:!0,inside:Prism.languages.javascript},{pattern:/((?:\b|\s|^)(?!(?:as|async|await|break|case|catch|class|const|continue|debugger|default|delete|do|else|enum|export|extends|finally|for|from|function|get|if|implements|import|in|instanceof|interface|let|new|null|of|package|private|protected|public|return|set|static|super|switch|this|throw|try|typeof|undefined|var|void|while|with|yield)(?![$\w\xA0-\uFFFF]))(?:[_$A-Za-z\xA0-\uFFFF][$\w\xA0-\uFFFF]*\s*)\(\s*|\]\s*\(\s*)(?!\s)(?:[^()]|\([^()]*\))+?(?=\s*\)\s*\{)/,lookbehind:!0,inside:Prism.languages.javascript}],constant:/\b[A-Z](?:[A-Z_]|\dx?)*\b/}),Prism.languages.insertBefore("javascript","string",{"template-string":{pattern:/`(?:\\[\s\S]|\${(?:[^{}]|{(?:[^{}]|{[^}]*})*})+}|(?!\${)[^\\`])*`/,greedy:!0,inside:{"template-punctuation":{pattern:/^`|`$/,alias:"string"},interpolation:{pattern:/((?:^|[^\\])(?:\\{2})*)\${(?:[^{}]|{(?:[^{}]|{[^}]*})*})+}/,lookbehind:!0,inside:{"interpolation-punctuation":{pattern:/^\${|}$/,alias:"punctuation"},rest:Prism.languages.javascript}},string:/[\s\S]+/}}}),Prism.languages.markup&&Prism.languages.markup.tag.addInlined("script","javascript"),Prism.languages.js=Prism.languages.javascript;
@@ -0,0 +1,88 @@
const config = require('../config')
const build = {
import: 'prism.css',
handler (file) {
if (file.path.includes('prism.css')) {
// 将标签名选择器和属性选择器转为 class 选择器(组件内仅支持 class 选择器)
file.contents = Buffer.from(file.contents.toString().replace(/pre([[)])/g, '.hl-pre$1').replace(/code/g, '.hl-code').replace(/\[class\*="?language-"?\]/g, '').replace(/:not[^,}]+[,}]*/g, '').replace(/\.token\./g, '.hl-'))
}
}
}
if (config.showLanguageName || config.showLineNumber) {
// pre 内部的 code 进行滚动,避免行号和语言名称跟随滚动
build.style = `.hl-pre {
position: relative;
}
.hl-code {
overflow: auto;
display: block;
}`
}
if (config.copyByLongPress) {
build.template = '<rich-text v-if="n.attrs&&n.attrs[\'data-content\']" :nodes="[n]" :data-content="n.attrs[\'data-content\']" :data-lang="n.attrs[\'data-lang\']" @longpress="copyCode" />'
build.methods = {
copyCode (e) {
uni.showActionSheet({
itemList: ['复制代码'],
success: () =>
uni.setClipboardData({
data: e.currentTarget.dataset.content
})
})
}
}
}
if (config.showLanguageName) {
build.style = (build.style || '') +
`.hl-language {
font-size: 12px;
font-weight: 600;
position: absolute;
right: 8px;
text-align: right;
top: 3px;
}
.hl-pre {
padding-top: 1.5em;
}`
}
if (config.showLineNumber) {
build.style = (build.style || '') +
`.hl-pre {
font-size: 14px;
padding-left: 3.8em;
counter-reset: linenumber;
}
.line-numbers-rows {
position: absolute;
pointer-events: none;
top: ${config.showLanguageName ? 1.5 : 1}em;
font-size: 100%;
left: 0;
width: 3em; /* works for line-numbers below 1000 lines */
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: 0.8em;
text-align: right;
}`
}
module.exports = build
+24
View File
@@ -0,0 +1,24 @@
功能:图片本地缓存
大小:*≈4KB*
作者:[@PentaTea](https://github.com/PentaTea)
支持平台:
| 微信小程序 | QQ 小程序 | 百度小程序 | 支付宝小程序 | 头条小程序 | uni-app |
|:---:|:---:|:---:|:---:|:---:|:---:|
| | | | | | √(仅支持 app 的 vue 页面) |
说明:
引入本插件后,会给组件添加一个 *img-cache* 属性,将该属性设置为 *true* 后,将自动下载引用的图片并将 *src* 属性更换为本地地址
同时在组件实例上挂载了 *imgCache* 对象,扩充缓存控制能力
*imgCache* 对象属性和方法:
| 属性 | 功能 |
|:---:|:---:|
| list | 当前缓存的 url 列表 |
| get(url) | 传入 url 获得本地地址 |
| delete(url) | 传入 url 删除缓存记录 |
| add(url) | 传入 url 并下载目标为缓存 |
| clear() | 清空所有缓存 |
!> 请尽量确保 *src* 中含有文件后缀名,不以后缀结尾也没关系,插件会从路径中推测合理的图片后缀,如果完全不包含后缀信息可能会无法保存到相册
+16
View File
@@ -0,0 +1,16 @@
module.exports = {
main: 'index.js',
platform: ['uni-app'],
handler (file, platform) {
if (platform === 'uni-app') {
// 添加 img-cache 属性
if (file.path.includes('mp-html.vue')) {
file.contents = Buffer.from(
file.contents
.toString()
.replace(/props\s*:\s*{/, 'props: {\n ImgCache: Boolean,')
)
}
}
}
}
+138
View File
@@ -0,0 +1,138 @@
const data = {
name: 'imgcache',
prefix: 'imgcache_'
}
function ImgCache (vm) {
this.vm = vm // 保存实例在其他周期使用
this.i = 0 // 用于标记第几张图
vm.imgCache = {
get list () {
return uni
.getStorageInfoSync()
.keys.filter((key) => key.startsWith(data.prefix))
.map((key) => key.split(data.prefix)[1])
},
get (url) {
return uni.getStorageSync(data.prefix + url)
},
delete (url) {
const path = uni.getStorageSync(data.prefix + url)
if (!path) return false
plus.io.resolveLocalFileSystemURL(path, (entry) => {
entry.remove()
})
uni.removeStorageSync(data.prefix + url)
return true
},
async add (url) {
const filename = await download(url)
if (filename) {
uni.setStorageSync(data.prefix + url, filename)
return 'file://' + plus.io.convertLocalFileSystemURL(filename)
}
return null
},
clear () {
uni
.getStorageInfoSync()
.keys.filter((key) => key.startsWith(data.prefix))
.forEach((key) => {
uni.removeStorageSync(key)
})
plus.io.resolveLocalFileSystemURL(`_doc/${data.name}/`, (entry) => {
entry.removeRecursively(
(entry) => {
console.log(`${data.name}缓存删除成功`, entry)
},
(e) => {
console.log(`${data.name}缓存删除失败`, e)
}
)
})
}
}
}
// #ifdef APP-PLUS
ImgCache.prototype.onParse = function (node, parser) {
// 启用本插件 && 解析图片标签 && 拥有src属性 && 是网络图片
if (
this.vm.ImgCache &&
node.name === 'img' &&
node.attrs.src &&
/^https?:\/\//.test(node.attrs.src)
) {
const src = node.attrs.src
node.attrs.src = ''
node.attrs.i = this.vm.imgList.length + this.i++
parser.expose()
async function getUrl (path) {
if (await resolveFile(path)) return path
const filename = await download(src)
filename && uni.setStorageSync(data.prefix + src, filename)
return filename
}
uni.getStorage({
key: data.prefix + src,
success: async (res) => {
const path = await getUrl(res.data)
const url = path
? 'file://' + plus.io.convertLocalFileSystemURL(path)
: src
node.attrs.src = url
this.vm.imgList[node.attrs.i] = path || src
},
fail: async () => {
const path = await getUrl()
const url = path
? 'file://' + plus.io.convertLocalFileSystemURL(path)
: src
node.attrs.src = url
this.vm.imgList[node.attrs.i] = path || src
}
})
}
}
const taskQueue = new Set()
function download (url) {
return new Promise((resolve) => {
if (taskQueue.has(url)) return
taskQueue.add(url)
const suffix = /.+\.(jpg|jpeg|png|bmp|gif|webp)/.exec(url)
const name = `${makeid(8)}_${Date.now()}${suffix ? '.' + suffix[1] : ''}`
const task = plus.downloader.createDownload(
url,
{ filename: `_doc/${data.name}/${name}` },
(download, status) => {
taskQueue.delete(url)
resolve(status === 200 ? download.filename : null)
}
)
task.start()
})
}
// 判断文件存在
function resolveFile (url) {
return new Promise((resolve) => {
plus.io.resolveLocalFileSystemURL(url, resolve, () => resolve(null))
})
}
// 生成uuid
function makeid (length) {
let result = ''
const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * characters.length))
}
return result
}
// #endif
module.exports = ImgCache
+16
View File
@@ -0,0 +1,16 @@
# latex
功能:渲染 *latex* 公式
大小:**≈300KB**
作者:[@Zeng-J](https://github.com/Zeng-J)
支持平台:
| 微信小程序 | QQ 小程序 | 百度小程序 | 支付宝小程序 | 头条小程序 | uni-app |
|:---:|:---:|:---:|:---:|:---:|:---:|
| √ | √ | √ | √ | √ | √ |
说明:
引入本插件后,会将 *$xxx$* 的文本内容按照 *latex* 规则进行解析和渲染
> 与 *editable* 插件共用时,编辑状态下,公式不会渲染,可以直接修改公式文本
> 本插件通过 [katex-mini](https://github.com/rojer95/katex-mini) 解析 *latex* 文本,[字体文件](https://github.com/KaTeX/KaTeX/tree/main/fonts) 建议自行转存
+14
View File
@@ -0,0 +1,14 @@
module.exports = {
import: 'katex.css',
handler (file) {
if (file.isBuffer()) {
let content = file.contents.toString()
if (file.basename === 'node.wxml') {
content = content.replace(/(n.?)\.name==='a'\|\|/g, "$1.name==='a'||$1.l||")
} else if (file.basename === 'node.vue') {
content = content.replace(/!handler.isInline\((.*?)\)/, '(n.l||!handler.isInline($1))')
}
file.contents = Buffer.from(content)
}
}
}
+80
View File
@@ -0,0 +1,80 @@
/**
* @fileoverview latex 插件
* katex.min.js来源 https://github.com/rojer95/katex-mini
*/
const parse = require('./katex.min')
function Latex () {
}
Latex.prototype.onParse = function (node, vm) {
// $...$包裹的内容为latex公式
if (!vm.options.editable && node.type === 'text' && node.text.includes('$')) {
const part = node.text.split(/(\${1,2})/)
const children = []
let status = 0
for (let i = 0; i < part.length; i++) {
if (i % 2 === 0) {
// 文本内容
if (part[i]) {
if (status === 0) {
children.push({
type: 'text',
text: part[i]
})
} else {
if (status === 1) {
// 行内公式
const nodes = parse.default(part[i])
children.push({
name: 'span',
attrs: {},
l: 'T',
f: 'display:inline-block',
children: nodes
})
} else {
// 块公式
const nodes = parse.default(part[i], {
displayMode: true
})
children.push({
name: 'div',
attrs: {
style: 'text-align:center'
},
children: nodes
})
}
}
}
} else {
// 分隔符
if (part[i] === '$' && part[i + 2] === '$') {
// 行内公式
status = 1
part[i + 2] = ''
} else if (part[i] === '$$' && part[i + 2] === '$$') {
// 块公式
status = 2
part[i + 2] = ''
} else {
if (part[i] && part[i] !== '$$') {
// 普通$符号
part[i + 1] = part[i] + part[i + 1]
}
// 重置状态
status = 0
}
}
}
delete node.type
delete node.text
node.name = 'span'
node.attrs = {}
node.children = children
}
}
module.exports = Latex
File diff suppressed because it is too large. Load diff
+1
View File
@@ -0,0 +1 @@
function t(){return function(e){var t={};function r(n){if(t[n])return t[n].exports;var a=t[n]={i:n,l:!1,exports:{}};return e[n].call(a.exports,a,a.exports,r),a.l=!0,a.exports}return r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var a in e)r.d(n,a,function(t){return e[t]}.bind(null,a));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=0)}([function(e,t,r){"use strict";var n=this&&this.__assign||function(){return(n=Object.assign||function(e){for(var t,r=1,n=arguments.length;r<n;r++)for(var a in t=arguments[r])Object.prototype.hasOwnProperty.call(t,a)&&(e[a]=t[a]);return e}).apply(this,arguments)},a=this&&this.__importDefault||function(e){return e&&e.__esModule?e:{default:e}};Object.defineProperty(t,"__esModule",{value:!0}),t.createClass=void 0;var i=a(r(1));r(2);var o=/([A-Z])/g,s={"&":"&amp;",">":"&gt;","<":"&lt;",'"':"&quot;","'":"&#x27;"},l=/[&><"']/g;function h(e){return String(e).replace(l,(function(e){return s[e]}))}var c=function(e){return"data:image/svg+xml,"+encodeURIComponent(e.replace(/\s+/g," "))};t.createClass=function(e){var t;return null!==(t=null==e?void 0:e.filter((function(e){return e})).join(" "))&&void 0!==t?t:""};var m=function(e,r){return e.map((function(e){var n,a=r;(null===(n=null==e?void 0:e.style)||void 0===n?void 0:n.color)&&(a=e.style.color);var s=void 0;e instanceof i.default.__domTree.Span&&(s="span"),e instanceof i.default.__domTree.Anchor&&(s="anchor"),e instanceof i.default.__domTree.LineNode&&(s="line"),e instanceof i.default.__domTree.PathNode&&(s="path"),e instanceof i.default.__domTree.SvgNode&&(s="svg",a&&(e.attributes.fill=a)),e instanceof i.default.__domTree.SymbolNode&&(s="text");var l=e.children&&e.children.length>0?m(e.children,a):[];return s?function(e,r,n){var a=!1;r.classes&&r.classes.length>0&&(a=!0);var i,s=h((0,t.createClass)(r.classes)),l="";for(var m in"text"===e&&r.italic>0&&(l+="margin-right:"+r.italic+"em;"),r.style)r.style.hasOwnProperty(m)&&(l+="".concat((i=m,i.replace(o,"-$1").toLowerCase()),":").concat(r.style[m],";"));l&&(a=!0);var u={};for(var p in r.attributes)r.attributes.hasOwnProperty(p)&&(u[p]=h(r.attributes[p]));if("span"===e)return{name:"span",attrs:{class:s+" katex-span",style:l},children:n};if("text"===e){var d=h(r.text);return a?{name:"span",attrs:{class:s,style:l},children:[{type:"text",text:d}]}:{type:"text",text:d}}if("svg"===e){var f=r.toMarkup();return{name:"img",attrs:{src:c(f),class:"katex-svg"}}}return null}(s,e,l):l})).reduce((function(e,t){return Array.isArray(t)?e.push.apply(e,t):e.push(t),e}),[]).filter((function(e){return!!e}))};t.default=function(e,t){void 0===t&&(t={});try{var r=i.default.__renderToDomTree(e,n(n({},t),{output:"html"}));return m([r])}catch(e){return[{name:"span",attrs:{style:"color:red;"},children:[{type:"text",text:e.message}]}]}}},function(e,t,r){var n;"undefined"!=typeof self&&self,n=function(){return function(){"use strict";var e={d:function(t,r){for(var n in r)e.o(r,n)&&!e.o(t,n)&&Object.defineProperty(t,n,{enumerable:!0,get:r[n]})},o:function(e,t){return Object.prototype.hasOwnProperty.call(e,t)}},t={};e.d(t,{default:function(){return Fn}});var r=function e(t,r){this.position=void 0;var n,a="KaTeX parse error: "+t,i=r&&r.loc;if(i&&i.start<=i.end){var o=i.lexer.input;n=i.start;var s=i.end;n===o.length?a+=" at end of input: ":a+=" at position "+(n+1)+": ";var l=o.slice(n,s).replace(/[^]/g,"$&̲");a+=(n>15?"…"+o.slice(n-15,n):o.slice(0,n))+l+(s+15<o.length?o.slice(s,s+15)+"…":o.slice(s))}var h=new Error(a);return h.name="ParseError",h.__proto__=e.prototype,h.position=n,h};r.prototype.__proto__=Error.prototype;var n=r,a=/([A-Z])/g,i={"&":"&amp;",">":"&gt;","<":"&lt;",'"':"&quot;","'":"&#x27;"},o=/[&><"']/g,s=function e(t){return"ordgroup"===t.type||"color"===t.type?1===t.body.length?e(t.body[0]):t:"font"===t.type?e(t.body):t},l={contains:function(e,t){return-1!==e.indexOf(t)},deflt:function(e,t){return void 0===e?t:e},escape:function(e){return String(e).replace(o,(function(e){return i[e]}))},hyphenate:function(e){return e.replace(a,"-$1").toLowerCase()},getBaseElem:s,isCharacterBox:function(e){var t=s(e);return"mathord"===t.type||"textord"===t.type||"atom"===t.type},protocolFromUrl:function(e){var t=/^\s*([^\\/#]*?)(?::|&#0*58|&#x0*3a)/i.exec(e);return null!=t?t[1]:"_relative"}},h={displayMode:{type:"boolean",description:"Render math in display mode, which puts the math in display style (so \\int and \\sum are large, Line truncated
+17
View File
@@ -0,0 +1,17 @@
# markdown
功能:渲染 *markdown*
大小:*≈37KB*
支持平台:
| 微信小程序 | QQ 小程序 | 百度小程序 | 支付宝小程序 | 头条小程序 | uni-app |
|:---:|:---:|:---:|:---:|:---:|:---:|
| √ | √ | √ | √ | √ | √ |
说明:
引入本插件后,会给组件添加一个 *markdown* 属性,将该属性设置为 *true* 后,即可通过 *content* 属性或 *setContent* 方法设置 *markdown* 内容即可
若开启 *use-anchor* 属性,所有标题 `*# xxx*` 都会被设置为锚点,通过链接 `[xxx](#xxx)` 可以直接跳转
> 本插件通过 [marked](https://github.com/markedjs/marked) 解析 *markdown* 文本,部分 *css* 摘选自 [github-markdown-css](https://github.com/sindresorhus/github-markdown-css)
> 本插件可以和 *highlight* 插件共用,实现 *markdown* 中代码块的高亮效果
+36
View File
@@ -0,0 +1,36 @@
/**
* @fileoverview markdown 插件
* Include marked (https://github.com/markedjs/marked)
* Include github-markdown-css (https://github.com/sindresorhus/github-markdown-css)
*/
const { marked } = require('./marked.min')
let index = 0
function Markdown (vm) {
this.vm = vm
vm._ids = {}
}
Markdown.prototype.onUpdate = function (content) {
if (this.vm.properties.markdown) {
// 解决中文标点符号后粗体失效的问题,增加零宽空格
content = content.replace(/\*\*([^*]+)\*\*([,。!?;:])/g, '**$1**&#8203;$2')
return marked(content)
}
}
Markdown.prototype.onParse = function (node, vm) {
if (vm.options.markdown) {
// 中文 id 需要转换,否则无法跳转
if (vm.options.useAnchor && node.attrs && /[\u4e00-\u9fa5]/.test(node.attrs.id)) {
const id = 't' + index++
this.vm._ids[node.attrs.id] = id
node.attrs.id = id
}
if (node.name === 'p' || node.name === 'table' || node.name === 'tr' || node.name === 'th' || node.name === 'td' || node.name === 'blockquote' || node.name === 'pre' || node.name === 'code') {
node.attrs.class = `md-${node.name} ${node.attrs.class || ''}`
}
}
}
module.exports = Markdown
+71
View File
@@ -0,0 +1,71 @@
/**
* marked v16.3.0 - a markdown parser
* Copyright (c) 2011-2025, Christopher Jeffrey. (MIT Licensed)
* https://github.com/markedjs/marked
*/
/**
* DO NOT EDIT THIS FILE
* The code in this file is generated from files in ./src/
*/
function L(){return{async:!1,breaks:!1,extensions:null,gfm:!0,hooks:null,pedantic:!1,renderer:null,silent:!1,tokenizer:null,walkTokens:null}}var O=L();function G(l){O=l}var E={exec:()=>null};function h(l,e=""){let t=typeof l=="string"?l:l.source,n={replace:(r,i)=>{let s=typeof i=="string"?i:i.source;return s=s.replace(m.caret,"$1"),t=t.replace(r,s),n},getRegex:()=>new RegExp(t,e)};return n}var m={codeRemoveIndent:/^(?: {1,4}| {0,3}\t)/gm,outputLinkReplace:/\\([\[\]])/g,indentCodeCompensation:/^(\s+)(?:```)/,beginningSpace:/^\s+/,endingHash:/#$/,startingSpaceChar:/^ /,endingSpaceChar:/ $/,nonSpaceChar:/[^ ]/,newLineCharGlobal:/\n/g,tabCharGlobal:/\t/g,multipleSpaceGlobal:/\s+/g,blankLine:/^[ \t]*$/,doubleBlankLine:/\n[ \t]*\n[ \t]*$/,blockquoteStart:/^ {0,3}>/,blockquoteSetextReplace:/\n {0,3}((?:=+|-+) *)(?=\n|$)/g,blockquoteSetextReplace2:/^ {0,3}>[ \t]?/gm,listReplaceTabs:/^\t+/,listReplaceNesting:/^ {1,4}(?=( {4})*[^ ])/g,listIsTask:/^\[[ xX]\] /,listReplaceTask:/^\[[ xX]\] +/,anyLine:/\n.*\n/,hrefBrackets:/^<(.*)>$/,tableDelimiter:/[:|]/,tableAlignChars:/^\||\| *$/g,tableRowBlankLine:/\n[ \t]*$/,tableAlignRight:/^ *-+: *$/,tableAlignCenter:/^ *:-+: *$/,tableAlignLeft:/^ *:-+ *$/,startATag:/^<a /i,endATag:/^<\/a>/i,startPreScriptTag:/^<(pre|code|kbd|script)(\s|>)/i,endPreScriptTag:/^<\/(pre|code|kbd|script)(\s|>)/i,startAngleBracket:/^</,endAngleBracket:/>$/,pedanticHrefTitle:/^([^'"]*[^\s])\s+(['"])(.*)\2/,unicodeAlphaNumeric:/[\p{L}\p{N}]/u,escapeTest:/[&<>"']/,escapeReplace:/[&<>"']/g,escapeTestNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/,escapeReplaceNoEncode:/[<>"']|&(?!(#\d{1,7}|#[Xx][a-fA-F0-9]{1,6}|\w+);)/g,unescapeTest:/&(#(?:\d+)|(?:#x[0-9A-Fa-f]+)|(?:\w+));?/ig,caret:/(^|[^\[])\^/g,percentDecode:/%25/g,findPipe:/\|/g,splitPipe:/ \|/,slashPipe:/\\\|/g,carriageReturn:/\r\n|\r/g,spaceLine:/^ +$/gm,notSpaceStart:/^\S*/,endingNewline:/\n$/,listItemRegex:l=>new RegExp(`^( {0,3}${l})((?:[ ][^\\n]*)?(?:\\n|$))`),nextBulletRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}(?:[*+-]|\\d{1,9}[.)])((?:[ ][^\\n]*)?(?:\\n|$))`),hrRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}((?:- *){3,}|(?:_ *){3,}|(?:\\* *){3,})(?:\\n+|$)`),fencesBeginRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}(?:\`\`\`|~~~)`),headingBeginRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}#`),htmlBeginRegex:l=>new RegExp(`^ {0,${Math.min(3,l-1)}}<(?:[a-z].*>|!--)`,"i")},xe=/^(?:[ \t]*(?:\n|$))+/,be=/^((?: {4}| {0,3}\t)[^\n]+(?:\n(?:[ \t]*(?:\n|$))*)?)+/,Re=/^ {0,3}(`{3,}(?=[^`\n]*(?:\n|$))|~{3,})([^\n]*)(?:\n|$)(?:|([\s\S]*?)(?:\n|$))(?: {0,3}\1[~`]* *(?=\n|$)|$)/,C=/^ {0,3}((?:-[\t ]*){3,}|(?:_[ \t]*){3,}|(?:\*[ \t]*){3,})(?:\n+|$)/,Oe=/^ {0,3}(#{1,6})(?=\s|$)(.*)(?:\n+|$)/,j=/(?:[*+-]|\d{1,9}[.)])/,se=/^(?!bull |blockCode|fences|blockquote|heading|html|table)((?:.|\n(?!\s*?\n|bull |blockCode|fences|blockquote|heading|html|table))+?)\n {0,3}(=+|-+) *(?:\n+|$)/,ie=h(se).replace(/bull/g,j).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/\|table/g,"").getRegex(),Te=h(se).replace(/bull/g,j).replace(/blockCode/g,/(?: {4}| {0,3}\t)/).replace(/fences/g,/ {0,3}(?:`{3,}|~{3,})/).replace(/blockquote/g,/ {0,3}>/).replace(/heading/g,/ {0,3}#{1,6}/).replace(/html/g,/ {0,3}<[^\n>]+>\n/).replace(/table/g,/ {0,3}\|?(?:[:\- ]*\|)+[\:\- ]*\n/).getRegex(),F=/^([^\n]+(?:\n(?!hr|heading|lheading|blockquote|fences|list|html|table| +\n)[^\n]+)*)/,we=/^[^\n]+/,Q=/(?!\s*\])(?:\\[\s\S]|[^\[\]\\])+/,ye=h(/^ {0,3}\[(label)\]: *(?:\n[ \t]*)?([^<\s][^\s]*|<.*?>)(?:(?: +(?:\n[ \t]*)?| *\n[ \t]*)(title))? *(?:\n+|$)/).replace("label",Q).replace("title",/(?:"(?:\\"?|[^"\\])*"|'[^'\n]*(?:\n[^'\n]+)*\n?'|\([^()]*\))/).getRegex(),Pe=h(/^( {0,3}bull)([ \t][^\n]+?)?(?:\n|$)/).replace(/bull/g,j).getRegex(),v="address|article|aside|base|basefont|blockquote|body|caption|center|col|colgroup|dd|details|dialog|dir|div|dl|dt|fieldset|figcaption|figure|footer|form|frame|frameset|h[1-6]|head|header|hr|html|iframe|legend|li|link|main|menu|menuitem|meta|nav|noframes|ol|optgroup|option|p|param|search|section|summary|table|tbody|td|tfoot|th|thead|title|tr|track|ul",U=/<!--(?:-?>|[\s\S]*?(?:-->|$))/,Se=h("^ {0,3}(?:<(script|pre|style|textarea)[\\s>][\\s\\S]*?(?:</\\1>[^\\n]*\\n+|$)|comment[^\\n]*(\\n+|$)|<\\?[\\s\\S]*?(?:\\?>\\n*|$)|<![A-Z][\\s\\S]*?(?:>\\n*|$)|<!\\[CDATA\\[[\\s\\S]*?(?:\\]\\]>\\n*|$)|</?(tag)(?: +|\\n|/?>)[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|<(?!script|pre|style|textarea)([a-z][\\w-]*)(?:attribute)*? */?>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$)|</(?!script|pre|style|textarea)[a-z][\\w-]*\\s*>(?=[ \\t]*(?:\\n|$))[\\s\\S]*?(?:(?:\\n[ ]*)+\\n|$))","i").replace("comment",U).replace("tag",v).replace("attribute",/ +[a-zA-Z:_][\w.:-]*(?: *= *"[^"\n]*"| *= *'[^'\n]*'| *= *[^\s"'=<>`]+)?/).getRegex(),oe=h(F).replace("hr",C).replace("heading"," {0,3}#{1,6}(?:\\s|$)").replace("|lheading","").replace("|table","").replLine truncated
]`).replace("lheading",ie).replace("|table","").replace("blockquote"," {0,3}>").replace("|fences","").replace("|list","").replace("|html","").replace("|tag","").getRegex()},Me=/^\\([!"#$%&'()*+,\-./:;<=>?@\[\]\\^_`{|}~])/,ze=/^(`+)([^`]|[^`][\s\S]*?[^`])\1(?!`)/,ae=/^( {2,}|\\)\n(?!\s*$)/,Ae=/^(`+|[^`])(?:(?= {2,}\n)|[\s\S]*?(?:(?=[\\<!\[`*_]|\b_|$)|[^ ](?= {2,}\n)))/,D=/[\p{P}\p{S}]/u,W=/[\s\p{P}\p{S}]/u,le=/[^\s\p{P}\p{S}]/u,Ee=h(/^((?![*_])punctSpace)/,"u").replace(/punctSpace/g,W).getRegex(),ue=/(?!~)[\p{P}\p{S}]/u,Ce=/(?!~)[\s\p{P}\p{S}]/u,Ie=/(?:[^\s\p{P}\p{S}]|~)/u,Be=/\[[^\[\]]*?\]\((?:\\[\s\S]|[^\\\(\)]|\((?:\\[\s\S]|[^\\\(\)])*\))*\)|`[^`]*?`|<(?! )[^<>]*?>/g,pe=/^(?:\*+(?:((?!\*)punct)|[^\s*]))|^_+(?:((?!_)punct)|([^\s_]))/,qe=h(pe,"u").replace(/punct/g,D).getRegex(),ve=h(pe,"u").replace(/punct/g,ue).getRegex(),ce="^[^_*]*?__[^_*]*?\\*[^_*]*?(?=__)|[^*]+(?=[^*])|(?!\\*)punct(\\*+)(?=[\\s]|$)|notPunctSpace(\\*+)(?!\\*)(?=punctSpace|$)|(?!\\*)punctSpace(\\*+)(?=notPunctSpace)|[\\s](\\*+)(?!\\*)(?=punct)|(?!\\*)punct(\\*+)(?!\\*)(?=punct)|notPunctSpace(\\*+)(?=notPunctSpace)",De=h(ce,"gu").replace(/notPunctSpace/g,le).replace(/punctSpace/g,W).replace(/punct/g,D).getRegex(),He=h(ce,"gu").replace(/notPunctSpace/g,Ie).replace(/punctSpace/g,Ce).replace(/punct/g,ue).getRegex(),Ze=h("^[^_*]*?\\*\\*[^_*]*?_[^_*]*?(?=\\*\\*)|[^_]+(?=[^_])|(?!_)punct(_+)(?=[\\s]|$)|notPunctSpace(_+)(?!_)(?=punctSpace|$)|(?!_)punctSpace(_+)(?=notPunctSpace)|[\\s](_+)(?!_)(?=punct)|(?!_)punct(_+)(?!_)(?=punct)","gu").replace(/notPunctSpace/g,le).replace(/punctSpace/g,W).replace(/punct/g,D).getRegex(),Ge=h(/\\(punct)/,"gu").replace(/punct/g,D).getRegex(),Ne=h(/^<(scheme:[^\s\x00-\x1f<>]*|email)>/).replace("scheme",/[a-zA-Z][a-zA-Z0-9+.-]{1,31}/).replace("email",/[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+(@)[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?![-_])/).getRegex(),je=h(U).replace("(?:-->|$)","-->").getRegex(),Fe=h("^comment|^</[a-zA-Z][\\w:-]*\\s*>|^<[a-zA-Z][\\w-]*(?:attribute)*?\\s*/?>|^<\\?[\\s\\S]*?\\?>|^<![a-zA-Z]+\\s[\\s\\S]*?>|^<!\\[CDATA\\[[\\s\\S]*?\\]\\]>").replace("comment",je).replace("attribute",/\s+[a-zA-Z:_][\w.:-]*(?:\s*=\s*"[^"]*"|\s*=\s*'[^']*'|\s*=\s*[^\s"'=<>`]+)?/).getRegex(),q=/(?:\[(?:\\[\s\S]|[^\[\]\\])*\]|\\[\s\S]|`[^`]*`|[^\[\]\\`])*?/,Qe=h(/^!?\[(label)\]\(\s*(href)(?:(?:[ \t]*(?:\n[ \t]*)?)(title))?\s*\)/).replace("label",q).replace("href",/<(?:\\.|[^\n<>\\])+>|[^ \t\n\x00-\x1f]*/).replace("title",/"(?:\\"?|[^"\\])*"|'(?:\\'?|[^'\\])*'|\((?:\\\)?|[^)\\])*\)/).getRegex(),he=h(/^!?\[(label)\]\[(ref)\]/).replace("label",q).replace("ref",Q).getRegex(),de=h(/^!?\[(ref)\](?:\[\])?/).replace("ref",Q).getRegex(),Ue=h("reflink|nolink(?!\\()","g").replace("reflink",he).replace("nolink",de).getRegex(),X={_backpedal:E,anyPunctuation:Ge,autolink:Ne,blockSkip:Be,br:ae,code:ze,del:E,emStrongLDelim:qe,emStrongRDelimAst:De,emStrongRDelimUnd:Ze,escape:Me,link:Qe,nolink:de,punctuation:Ee,reflink:he,reflinkSearch:Ue,tag:Fe,text:Ae,url:E},Ke={...X,link:h(/^!?\[(label)\]\((.*?)\)/).replace("label",q).getRegex(),reflink:h(/^!?\[(label)\]\s*\[([^\]]*)\]/).replace("label",q).getRegex()},N={...X,emStrongRDelimAst:He,emStrongLDelim:ve,url:h(/^((?:ftp|https?):\/\/|www\.)(?:[a-zA-Z0-9\-]+\.?)+[^\s<]*|^email/,"i").replace("email",/[A-Za-z0-9._+-]+(@)[a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]*[a-zA-Z0-9])+(?![-_])/).getRegex(),_backpedal:/(?:[^?!.,:;*_'"~()&]+|\([^)]*\)|&(?![a-zA-Z0-9]+;$)|[?!.,:;*_'"~)]+(?!$))+/,del:/^(~~?)(?=[^\s~])((?:\\[\s\S]|[^\\])*?(?:\\[\s\S]|[^\s~\\]))\1(?=[^~]|$)/,text:/^([`~]+|[^`~])(?:(?= {2,}\n)|(?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)|[\s\S]*?(?:(?=[\\<!\[`*~_]|\b_|https?:\/\/|ftp:\/\/|www\.|$)|[^ ](?= {2,}\n)|[^a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-](?=[a-zA-Z0-9.!#$%&'*+\/=?_`{\|}~-]+@)))/},We={...N,br:h(ae).replace("{2,}","*").getRegex(),text:h(N.text).replace("\\b_","\\b_| {2,}\\n").replace(/\{2,\}/g,"*").getRegex()},I={normal:K,gfm:_e,pedantic:Le},M={normal:X,gfm:N,breaks:We,pedantic:Ke};var Xe={"&":"&amp;","<":"&lt;",">":"&gt;",'"':"&quot;","'":"&#39;"},ke=l=>Xe[l];function w(l,e){if(e){if(m.escapeTest.test(l))return l.replace(m.escapeReplace,ke)}else if(m.escapeTestNoEncode.test(l))return l.replace(m.escapeReplaceNoEncode,ke);return l}function J(l){try{l=encodeURI(l).replace(m.percentDecode,"%")}catch{return null}return l}function V(l,e){let t=l.replace(m.findPipe,(i,s,o)=>{let a=!1,u=s;for(;--u>=0&&o[u]==="\\";)a=!a;return a?"|":" |"}),n=t.split(m.splitPipe),r=0;if(n[0].trim()||n.shift(),n.length>0&&!n.at(-1)?.trim()&&n.pop(),e)if(n.length>e)n.splice(e);else for(;n.length<e;)n.push("");for(;r<n.length;r++)n[r]=n[r].trim().replace(m.slashPipe,"|");return n}function z(l,e,t){let n=l.length;if(n===0)return"";let r=0;for(;r<n;){let i=l.charAt(n-r-1);if(i===e&&!t)r++;else if(i!==e&&t)r++;else break}return l.slice(0,n-r)}function ge(l,e){if(l.indexOf(e[1])===-1)return-1;let t=0;for(let n=0;n<l.length;n++)if(l[n]==="\\")n++;else if(l[n]===e[0])t++;else if(l[n]===e[1]&&(Line truncated
`).map(i=>{let s=i.match(t.other.beginningSpace);if(s===null)return i;let[o]=s;return o.length>=r.length?i.slice(r.length):i}).join(`
`)}var y=class{options;rules;lexer;constructor(e){this.options=e||O}space(e){let t=this.rules.block.newline.exec(e);if(t&&t[0].length>0)return{type:"space",raw:t[0]}}code(e){let t=this.rules.block.code.exec(e);if(t){let n=t[0].replace(this.rules.other.codeRemoveIndent,"");return{type:"code",raw:t[0],codeBlockStyle:"indented",text:this.options.pedantic?n:z(n,`
`)}}}fences(e){let t=this.rules.block.fences.exec(e);if(t){let n=t[0],r=Je(n,t[3]||"",this.rules);return{type:"code",raw:n,lang:t[2]?t[2].trim().replace(this.rules.inline.anyPunctuation,"$1"):t[2],text:r}}}heading(e){let t=this.rules.block.heading.exec(e);if(t){let n=t[2].trim();if(this.rules.other.endingHash.test(n)){let r=z(n,"#");(this.options.pedantic||!r||this.rules.other.endingSpaceChar.test(r))&&(n=r.trim())}return{type:"heading",raw:t[0],depth:t[1].length,text:n,tokens:this.lexer.inline(n)}}}hr(e){let t=this.rules.block.hr.exec(e);if(t)return{type:"hr",raw:z(t[0],`
`)}}blockquote(e){let t=this.rules.block.blockquote.exec(e);if(t){let n=z(t[0],`
`).split(`
`),r="",i="",s=[];for(;n.length>0;){let o=!1,a=[],u;for(u=0;u<n.length;u++)if(this.rules.other.blockquoteStart.test(n[u]))a.push(n[u]),o=!0;else if(!o)a.push(n[u]);else break;n=n.slice(u);let p=a.join(`
`),c=p.replace(this.rules.other.blockquoteSetextReplace,`
$1`).replace(this.rules.other.blockquoteSetextReplace2,"");r=r?`${r}
${p}`:p,i=i?`${i}
${c}`:c;let f=this.lexer.state.top;if(this.lexer.state.top=!0,this.lexer.blockTokens(c,s,!0),this.lexer.state.top=f,n.length===0)break;let k=s.at(-1);if(k?.type==="code")break;if(k?.type==="blockquote"){let x=k,g=x.raw+`
`+n.join(`
`),T=this.blockquote(g);s[s.length-1]=T,r=r.substring(0,r.length-x.raw.length)+T.raw,i=i.substring(0,i.length-x.text.length)+T.text;break}else if(k?.type==="list"){let x=k,g=x.raw+`
`+n.join(`
`),T=this.list(g);s[s.length-1]=T,r=r.substring(0,r.length-k.raw.length)+T.raw,i=i.substring(0,i.length-x.raw.length)+T.raw,n=g.substring(s.at(-1).raw.length).split(`
`);continue}}return{type:"blockquote",raw:r,tokens:s,text:i}}}list(e){let t=this.rules.block.list.exec(e);if(t){let n=t[1].trim(),r=n.length>1,i={type:"list",raw:"",ordered:r,start:r?+n.slice(0,-1):"",loose:!1,items:[]};n=r?`\\d{1,9}\\${n.slice(-1)}`:`\\${n}`,this.options.pedantic&&(n=r?n:"[*+-]");let s=this.rules.other.listItemRegex(n),o=!1;for(;e;){let u=!1,p="",c="";if(!(t=s.exec(e))||this.rules.block.hr.test(e))break;p=t[0],e=e.substring(p.length);let f=t[2].split(`
`,1)[0].replace(this.rules.other.listReplaceTabs,H=>" ".repeat(3*H.length)),k=e.split(`
`,1)[0],x=!f.trim(),g=0;if(this.options.pedantic?(g=2,c=f.trimStart()):x?g=t[1].length+1:(g=t[2].search(this.rules.other.nonSpaceChar),g=g>4?1:g,c=f.slice(g),g+=t[1].length),x&&this.rules.other.blankLine.test(k)&&(p+=k+`
`,e=e.substring(k.length+1),u=!0),!u){let H=this.rules.other.nextBulletRegex(g),ee=this.rules.other.hrRegex(g),te=this.rules.other.fencesBeginRegex(g),ne=this.rules.other.headingBeginRegex(g),me=this.rules.other.htmlBeginRegex(g);for(;e;){let Z=e.split(`
`,1)[0],A;if(k=Z,this.options.pedantic?(k=k.replace(this.rules.other.listReplaceNesting," "),A=k):A=k.replace(this.rules.other.tabCharGlobal," "),te.test(k)||ne.test(k)||me.test(k)||H.test(k)||ee.test(k))break;if(A.search(this.rules.other.nonSpaceChar)>=g||!k.trim())c+=`
`+A.slice(g);else{if(x||f.replace(this.rules.other.tabCharGlobal," ").search(this.rules.other.nonSpaceChar)>=4||te.test(f)||ne.test(f)||ee.test(f))break;c+=`
`+k}!x&&!k.trim()&&(x=!0),p+=Z+`
`,e=e.substring(Z.length+1),f=A.slice(g)}}i.loose||(o?i.loose=!0:this.rules.other.doubleBlankLine.test(p)&&(o=!0));let T=null,Y;this.options.gfm&&(T=this.rules.other.listIsTask.exec(c),T&&(Y=T[0]!=="[ ] ",c=c.replace(this.rules.other.listReplaceTask,""))),i.items.push({type:"list_item",raw:p,task:!!T,checked:Y,loose:!1,text:c,tokens:[]}),i.raw+=p}let a=i.items.at(-1);if(a)a.raw=a.raw.trimEnd(),a.text=a.text.trimEnd();else return;i.raw=i.raw.trimEnd();for(let u=0;u<i.items.length;u++)if(this.lexer.state.top=!1,i.items[u].tokens=this.lexer.blockTokens(i.items[u].text,[]),!i.loose){let p=i.items[u].tokens.filter(f=>f.type==="space"),c=p.length>0&&p.some(f=>this.rules.other.anyLine.test(f.raw));i.loose=c}if(i.loose)for(let u=0;u<i.items.length;u++)i.items[u].loose=!0;return i}}html(e){let t=this.rules.block.html.exec(e);if(t)return{type:"html",block:!0,raw:t[0],pre:t[1]==="pre"||t[1]==="script"||t[1]==="style",text:t[0]}}def(e){let t=this.rules.block.def.exec(e);if(t){let n=t[1].toLowerCase().replace(this.rules.other.multipleSpaceGlobal," "),r=t[2]?t[2].replace(this.rules.other.hrefBrackets,"$1").replace(this.rules.inline.anyPunctuation,"$1"):"",i=t[3]?t[3].substring(1,t[3].length-1).replace(this.rules.inline.anyPunctuation,"$1"):t[3];return{type:"def",tag:n,raw:t[0],href:r,title:i}}}table(e){let t=this.rules.block.table.exec(e);if(!t||!this.rules.other.tableDelimiter.test(t[2]))return;let n=V(t[1]),r=t[2].replace(this.rules.other.tableAlignChars,"").split("|"),i=t[3]?.trim()?t[3].replace(this.rules.other.tableRowBlankLine,"").split(`
`):[],s={type:"table",raw:t[0],header:[],align:[],rows:[]};if(n.length===r.length){for(let o of r)this.rules.other.tableAlignRight.test(o)?s.align.push("right"):this.rules.other.tableAlignCenter.test(o)?s.align.push("center"):this.rules.other.tableAlignLeft.test(o)?s.align.push("left"):s.align.push(null);for(let o=0;o<n.length;o++)s.header.push({text:n[o],tokens:this.lexer.inline(n[o]),header:!0,align:s.align[o]});for(let o of i)s.rows.push(V(o,s.header.length).map((a,u)=>({text:a,tokens:this.lexer.inline(a),header:!1,align:s.align[u]})));return s}}lheading(e){let t=this.rules.block.lheading.exec(e);if(t)return{type:"heading",raw:t[0],depth:t[2].charAt(0)==="="?1:2,text:t[1],tokens:this.lexer.inline(t[1])}}paragraph(e){let t=this.rules.block.paragraph.exec(e);if(t){let n=t[1].charAt(t[1].length-1)===`
`?t[1].slice(0,-1):t[1];return{type:"paragraph",raw:t[0],text:n,tokens:this.lexer.inline(n)}}}text(e){let t=this.rules.block.text.exec(e);if(t)return{type:"text",raw:t[0],text:t[0],tokens:this.lexer.inline(t[0])}}escape(e){let t=this.rules.inline.escape.exec(e);if(t)return{type:"escape",raw:t[0],text:t[1]}}tag(e){let t=this.rules.inline.tag.exec(e);if(t)return!this.lexer.state.inLink&&this.rules.other.startATag.test(t[0])?this.lexer.state.inLink=!0:this.lexer.state.inLink&&this.rules.other.endATag.test(t[0])&&(this.lexer.state.inLink=!1),!this.lexer.state.inRawBlock&&this.rules.other.startPreScriptTag.test(t[0])?this.lexer.state.inRawBlock=!0:this.lexer.state.inRawBlock&&this.rules.other.endPreScriptTag.test(t[0])&&(this.lexer.state.inRawBlock=!1),{type:"html",raw:t[0],inLink:this.lexer.state.inLink,inRawBlock:this.lexer.state.inRawBlock,block:!1,text:t[0]}}link(e){let t=this.rules.inline.link.exec(e);if(t){let n=t[2].trim();if(!this.options.pedantic&&this.rules.other.startAngleBracket.test(n)){if(!this.rules.other.endAngleBracket.test(n))return;let s=z(n.slice(0,-1),"\\");if((n.length-s.length)%2===0)return}else{let s=ge(t[2],"()");if(s===-2)return;if(s>-1){let a=(t[0].indexOf("!")===0?5:4)+t[1].length+s;t[2]=t[2].substring(0,s),t[0]=t[0].substring(0,a).trim(),t[3]=""}}let r=t[2],i="";if(this.options.pedantic){let s=this.rules.other.pedanticHrefTitle.exec(r);s&&(r=s[1],i=s[3])}else i=t[3]?t[3].slice(1,-1):"";return r=r.trim(),this.rules.other.startAngleBracket.test(r)&&(this.options.pedantic&&!this.rules.other.endAngleBracket.test(n)?r=r.slice(1):r=r.slice(1,-1)),fe(t,{href:r&&r.replace(this.rules.inline.anyPunctuation,"$1"),title:i&&i.replace(this.rules.inline.anyPunctuation,"$1")},t[0],this.lexer,this.rules)}}reflink(e,t){let n;if((n=this.rules.inline.reflink.exec(e))||(n=this.rules.inline.nolink.exec(e))){let r=(n[2]||n[1]).replace(this.rules.other.multipleSpaceGlobal," "),i=t[r.toLowerCase()];if(!i){let s=n[0].charAt(0);return{type:"text",raw:s,text:s}}return fe(n,i,n[0],this.lexer,this.rules)}}emStrong(e,t,n=""){let r=this.rules.inline.emStrongLDelim.exec(e);if(!r||r[3]&&n.match(this.rules.other.unicodeAlphaNumeric))return;if(!(r[1]||r[2]||"")||!n||this.rules.inline.punctuation.exec(n)){let s=[...r[0]].length-1,o,a,u=s,p=0,c=r[0][0]==="*"?this.rules.inline.emStrongRDelimAst:this.rules.inline.emStrongRDelimUnd;for(c.lastIndex=0,t=t.slice(-1*e.length+s);(r=c.exec(t))!=null;){if(o=r[1]||r[2]||r[3]||r[4]||r[5]||r[6],!o)continue;if(a=[...o].length,r[3]||r[4]){u+=a;continue}else if((r[5]||r[6])&&s%3&&!((s+a)%3)){p+=a;continue}if(u-=a,u>0)continue;a=Math.min(a,a+u+p);let f=[...r[0]][0].length,k=e.slice(0,s+r.index+f+a);if(Math.min(s,a)%2){let g=k.slice(1,-1);return{type:"em",raw:k,text:g,tokens:this.lexer.inlineTokens(g)}}let x=k.slice(2,-2);return{type:"strong",raw:k,text:x,tokens:this.lexer.inlineTokens(x)}}}}codespan(e){let t=this.rules.inline.code.exec(e);if(t){let n=t[2].replace(this.rules.other.newLineCharGlobal," "),r=this.rules.other.nonSpaceChar.test(n),i=this.rules.other.startingSpaceChar.test(n)&&this.rules.other.endingSpaceChar.test(n);return r&&i&&(n=n.substring(1,n.length-1)),{type:"codespan",raw:t[0],text:n}}}br(e){let t=this.rules.inline.br.exec(e);if(t)return{type:"br",raw:t[0]}}del(e){let t=this.rules.inline.del.exec(e);if(t)return{type:"del",raw:t[0],text:t[2],tokens:this.lexer.inlineTokens(t[2])}}autolink(e){let t=this.rules.inline.autolink.exec(e);if(t){let n,r;return t[2]==="@"?(n=t[1],r="mailto:"+n):(n=t[1],r=n),{type:"link",raw:t[0],text:n,href:r,tokens:[{type:"text",raw:n,text:n}]}}}url(e){let t;if(t=this.rules.inline.url.exec(e)){let n,r;if(t[2]==="@")n=t[0],r="mailto:"+n;else{let i;do i=t[0],t[0]=this.rules.inline._backpedal.exec(t[0])?.[0]??"";while(i!==t[0]);n=t[0],t[1]==="www."?r="http://"+t[0]:r=t[0]}return{type:"link",raw:t[0],text:n,href:r,tokens:[{type:"text",raw:n,text:n}]}}}inlineText(e){let t=this.rules.inline.text.exec(e);if(t){let n=this.lexer.state.inRawBlock;return{type:"text",raw:t[0],text:t[0],escaped:n}}}};var b=class l{tokens;options;state;tokenizer;inlineQueue;constructor(e){this.tokens=[],this.tokens.links=Object.create(null),this.options=e||O,this.options.tokenizer=this.options.tokenizer||new y,this.tokenizer=this.options.tokenizer,this.tokenizer.options=this.options,this.tokenizer.lexer=this,this.inlineQueue=[],this.state={inLink:!1,inRawBlock:!1,top:!0};let t={other:m,block:I.normal,inline:M.normal};this.options.pedantic?(t.block=I.pedantic,t.inline=M.pedantic):this.options.gfm&&(t.block=I.gfm,this.options.breaks?t.inline=M.breaks:t.inline=M.gfm),this.tokenizer.rules=t}static get rules(){return{block:I,inline:M}}static lex(e,t){return new l(t).lex(e)}static lexInline(e,t){return new l(t).inlineTokens(e)}lex(e){e=e.replace(m.carriageReturn,`
`),this.blockTokens(e,this.tokens);for(let t=0;t<this.inlineQueue.length;t++){let n=this.inlineQueue[t];this.inlineTokens(n.src,n.tokens)}return this.inlineQueue=[],this.tokens}blockTokens(e,t=[],n=!1){for(this.options.pedantic&&(e=e.replace(m.tabCharGlobal," ").replace(m.spaceLine,""));e;){let r;if(this.options.extensions?.block?.some(s=>(r=s.call({lexer:this},e,t))?(e=e.substring(r.raw.length),t.push(r),!0):!1))continue;if(r=this.tokenizer.space(e)){e=e.substring(r.raw.length);let s=t.at(-1);r.raw.length===1&&s!==void 0?s.raw+=`
`:t.push(r);continue}if(r=this.tokenizer.code(e)){e=e.substring(r.raw.length);let s=t.at(-1);s?.type==="paragraph"||s?.type==="text"?(s.raw+=(s.raw.endsWith(`
`)?"":`
`)+r.raw,s.text+=`
`+r.text,this.inlineQueue.at(-1).src=s.text):t.push(r);continue}if(r=this.tokenizer.fences(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.heading(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.hr(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.blockquote(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.list(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.html(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.def(e)){e=e.substring(r.raw.length);let s=t.at(-1);s?.type==="paragraph"||s?.type==="text"?(s.raw+=(s.raw.endsWith(`
`)?"":`
`)+r.raw,s.text+=`
`+r.raw,this.inlineQueue.at(-1).src=s.text):this.tokens.links[r.tag]||(this.tokens.links[r.tag]={href:r.href,title:r.title},t.push(r));continue}if(r=this.tokenizer.table(e)){e=e.substring(r.raw.length),t.push(r);continue}if(r=this.tokenizer.lheading(e)){e=e.substring(r.raw.length),t.push(r);continue}let i=e;if(this.options.extensions?.startBlock){let s=1/0,o=e.slice(1),a;this.options.extensions.startBlock.forEach(u=>{a=u.call({lexer:this},o),typeof a=="number"&&a>=0&&(s=Math.min(s,a))}),s<1/0&&s>=0&&(i=e.substring(0,s+1))}if(this.state.top&&(r=this.tokenizer.paragraph(i))){let s=t.at(-1);n&&s?.type==="paragraph"?(s.raw+=(s.raw.endsWith(`
`)?"":`
`)+r.raw,s.text+=`
`+r.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=s.text):t.push(r),n=i.length!==e.length,e=e.substring(r.raw.length);continue}if(r=this.tokenizer.text(e)){e=e.substring(r.raw.length);let s=t.at(-1);s?.type==="text"?(s.raw+=(s.raw.endsWith(`
`)?"":`
`)+r.raw,s.text+=`
`+r.text,this.inlineQueue.pop(),this.inlineQueue.at(-1).src=s.text):t.push(r);continue}if(e){let s="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(s);break}else throw new Error(s)}}return this.state.top=!0,t}inline(e,t=[]){return this.inlineQueue.push({src:e,tokens:t}),t}inlineTokens(e,t=[]){let n=e,r=null;if(this.tokens.links){let o=Object.keys(this.tokens.links);if(o.length>0)for(;(r=this.tokenizer.rules.inline.reflinkSearch.exec(n))!=null;)o.includes(r[0].slice(r[0].lastIndexOf("[")+1,-1))&&(n=n.slice(0,r.index)+"["+"a".repeat(r[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.reflinkSearch.lastIndex))}for(;(r=this.tokenizer.rules.inline.anyPunctuation.exec(n))!=null;)n=n.slice(0,r.index)+"++"+n.slice(this.tokenizer.rules.inline.anyPunctuation.lastIndex);for(;(r=this.tokenizer.rules.inline.blockSkip.exec(n))!=null;)n=n.slice(0,r.index)+"["+"a".repeat(r[0].length-2)+"]"+n.slice(this.tokenizer.rules.inline.blockSkip.lastIndex);n=this.options.hooks?.emStrongMask?.call({lexer:this},n)??n;let i=!1,s="";for(;e;){i||(s=""),i=!1;let o;if(this.options.extensions?.inline?.some(u=>(o=u.call({lexer:this},e,t))?(e=e.substring(o.raw.length),t.push(o),!0):!1))continue;if(o=this.tokenizer.escape(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.tag(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.link(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.reflink(e,this.tokens.links)){e=e.substring(o.raw.length);let u=t.at(-1);o.type==="text"&&u?.type==="text"?(u.raw+=o.raw,u.text+=o.text):t.push(o);continue}if(o=this.tokenizer.emStrong(e,n,s)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.codespan(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.br(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.del(e)){e=e.substring(o.raw.length),t.push(o);continue}if(o=this.tokenizer.autolink(e)){e=e.substring(o.raw.length),t.push(o);continue}if(!this.state.inLink&&(o=this.tokenizer.url(e))){e=e.substring(o.raw.length),t.push(o);continue}let a=e;if(this.options.extensions?.startInline){let u=1/0,p=e.slice(1),c;this.options.extensions.startInline.forEach(f=>{c=f.call({lexer:this},p),typeof c=="number"&&c>=0&&(u=Math.min(u,c))}),u<1/0&&u>=0&&(a=e.substring(0,u+1))}if(o=this.tokenizer.inlineText(a)){e=e.substring(o.raw.length),o.raw.slice(-1)!=="_"&&(s=o.raw.slice(-1)),i=!0;let u=t.at(-1);u?.type==="text"?(u.raw+=o.raw,u.text+=o.text):t.push(o);continue}if(e){let u="Infinite loop on byte: "+e.charCodeAt(0);if(this.options.silent){console.error(u);break}else throw new Error(u)}}return t}};var P=class{options;parser;constructor(e){this.options=e||O}space(e){return""}code({text:e,lang:t,escaped:n}){let r=(t||"").match(m.notSpaceStart)?.[0],i=e.replace(m.endingNewline,"")+`
`;return r?'<pre><code class="language-'+w(r)+'">'+(n?i:w(i,!0))+`</code></pre>
`:"<pre><code>"+(n?i:w(i,!0))+`</code></pre>
`}blockquote({tokens:e}){return`<blockquote>
${this.parser.parse(e)}</blockquote>
`}html({text:e}){return e}def(e){return""}heading({tokens:e,depth:t}){return`<h${t}>${this.parser.parseInline(e)}</h${t}>
`}hr(e){return`<hr>
`}list(e){let t=e.ordered,n=e.start,r="";for(let o=0;o<e.items.length;o++){let a=e.items[o];r+=this.listitem(a)}let i=t?"ol":"ul",s=t&&n!==1?' start="'+n+'"':"";return"<"+i+s+`>
`+r+"</"+i+`>
`}listitem(e){let t="";if(e.task){let n=this.checkbox({checked:!!e.checked});e.loose?e.tokens[0]?.type==="paragraph"?(e.tokens[0].text=n+" "+e.tokens[0].text,e.tokens[0].tokens&&e.tokens[0].tokens.length>0&&e.tokens[0].tokens[0].type==="text"&&(e.tokens[0].tokens[0].text=n+" "+w(e.tokens[0].tokens[0].text),e.tokens[0].tokens[0].escaped=!0)):e.tokens.unshift({type:"text",raw:n+" ",text:n+" ",escaped:!0}):t+=n+" "}return t+=this.parser.parse(e.tokens,!!e.loose),`<li>${t}</li>
`}checkbox({checked:e}){return"<input "+(e?'checked="" ':"")+'disabled="" type="checkbox">'}paragraph({tokens:e}){return`<p>${this.parser.parseInline(e)}</p>
`}table(e){let t="",n="";for(let i=0;i<e.header.length;i++)n+=this.tablecell(e.header[i]);t+=this.tablerow({text:n});let r="";for(let i=0;i<e.rows.length;i++){let s=e.rows[i];n="";for(let o=0;o<s.length;o++)n+=this.tablecell(s[o]);r+=this.tablerow({text:n})}return r&&(r=`<tbody>${r}</tbody>`),`<table>
<thead>
`+t+`</thead>
`+r+`</table>
`}tablerow({text:e}){return`<tr>
${e}</tr>
`}tablecell(e){let t=this.parser.parseInline(e.tokens),n=e.header?"th":"td";return(e.align?`<${n} align="${e.align}">`:`<${n}>`)+t+`</${n}>
`}strong({tokens:e}){return`<strong>${this.parser.parseInline(e)}</strong>`}em({tokens:e}){return`<em>${this.parser.parseInline(e)}</em>`}codespan({text:e}){return`<code>${w(e,!0)}</code>`}br(e){return"<br>"}del({tokens:e}){return`<del>${this.parser.parseInline(e)}</del>`}link({href:e,title:t,tokens:n}){let r=this.parser.parseInline(n),i=J(e);if(i===null)return r;e=i;let s='<a href="'+e+'"';return t&&(s+=' title="'+w(t)+'"'),s+=">"+r+"</a>",s}image({href:e,title:t,text:n,tokens:r}){r&&(n=this.parser.parseInline(r,this.parser.textRenderer));let i=J(e);if(i===null)return w(n);e=i;let s=`<img src="${e}" alt="${n}"`;return t&&(s+=` title="${w(t)}"`),s+=">",s}text(e){return"tokens"in e&&e.tokens?this.parser.parseInline(e.tokens):"escaped"in e&&e.escaped?e.text:w(e.text)}};var $=class{strong({text:e}){return e}em({text:e}){return e}codespan({text:e}){return e}del({text:e}){return e}html({text:e}){return e}text({text:e}){return e}link({text:e}){return""+e}image({text:e}){return""+e}br(){return""}};var R=class l{options;renderer;textRenderer;constructor(e){this.options=e||O,this.options.renderer=this.options.renderer||new P,this.renderer=this.options.renderer,this.renderer.options=this.options,this.renderer.parser=this,this.textRenderer=new $}static parse(e,t){return new l(t).parse(e)}static parseInline(e,t){return new l(t).parseInline(e)}parse(e,t=!0){let n="";for(let r=0;r<e.length;r++){let i=e[r];if(this.options.extensions?.renderers?.[i.type]){let o=i,a=this.options.extensions.renderers[o.type].call({parser:this},o);if(a!==!1||!["space","hr","heading","code","table","blockquote","list","html","def","paragraph","text"].includes(o.type)){n+=a||"";continue}}let s=i;switch(s.type){case"space":{n+=this.renderer.space(s);continue}case"hr":{n+=this.renderer.hr(s);continue}case"heading":{n+=this.renderer.heading(s);continue}case"code":{n+=this.renderer.code(s);continue}case"table":{n+=this.renderer.table(s);continue}case"blockquote":{n+=this.renderer.blockquote(s);continue}case"list":{n+=this.renderer.list(s);continue}case"html":{n+=this.renderer.html(s);continue}case"def":{n+=this.renderer.def(s);continue}case"paragraph":{n+=this.renderer.paragraph(s);continue}case"text":{let o=s,a=this.renderer.text(o);for(;r+1<e.length&&e[r+1].type==="text";)o=e[++r],a+=`
`+this.renderer.text(o);t?n+=this.renderer.paragraph({type:"paragraph",raw:a,text:a,tokens:[{type:"text",raw:a,text:a,escaped:!0}]}):n+=a;continue}default:{let o='Token with "'+s.type+'" type was not found.';if(this.options.silent)return console.error(o),"";throw new Error(o)}}}return n}parseInline(e,t=this.renderer){let n="";for(let r=0;r<e.length;r++){let i=e[r];if(this.options.extensions?.renderers?.[i.type]){let o=this.options.extensions.renderers[i.type].call({parser:this},i);if(o!==!1||!["escape","html","link","image","strong","em","codespan","br","del","text"].includes(i.type)){n+=o||"";continue}}let s=i;switch(s.type){case"escape":{n+=t.text(s);break}case"html":{n+=t.html(s);break}case"link":{n+=t.link(s);break}case"image":{n+=t.image(s);break}case"strong":{n+=t.strong(s);break}case"em":{n+=t.em(s);break}case"codespan":{n+=t.codespan(s);break}case"br":{n+=t.br(s);break}case"del":{n+=t.del(s);break}case"text":{n+=t.text(s);break}default:{let o='Token with "'+s.type+'" type was not found.';if(this.options.silent)return console.error(o),"";throw new Error(o)}}}return n}};var S=class{options;block;constructor(e){this.options=e||O}static passThroughHooks=new Set(["preprocess","postprocess","processAllTokens","emStrongMask"]);static passThroughHooksRespectAsync=new Set(["preprocess","postprocess","processAllTokens"]);preprocess(e){return e}postprocess(e){return e}processAllTokens(e){return e}emStrongMask(e){return e}provideLexer(){return this.block?b.lex:b.lexInline}provideParser(){return this.block?R.parse:R.parseInline}};var B=class{defaults=L();options=this.setOptions;parse=this.parseMarkdown(!0);parseInline=this.parseMarkdown(!1);Parser=R;Renderer=P;TextRenderer=$;Lexer=b;Tokenizer=y;Hooks=S;constructor(...e){this.use(...e)}walkTokens(e,t){let n=[];for(let r of e)switch(n=n.concat(t.call(this,r)),r.type){case"table":{let i=r;for(let s of i.header)n=n.concat(this.walkTokens(s.tokens,t));for(let s of i.rows)for(let o of s)n=n.concat(this.walkTokens(o.tokens,t));break}case"list":{let i=r;n=n.concat(this.walkTokens(i.items,t));break}default:{let i=r;this.defaults.extensions?.childTokens?.[i.type]?this.defaults.extensions.childTokens[i.type].forEach(s=>{let o=i[s].flat(1/0);n=n.concat(this.walkTokens(o,t))}):i.tokens&&(n=n.concat(this.walkTokens(i.tokens,t)))}}return n}use(...e){let t=this.defaults.extensions||{renderers:{},childTokens:{}};return e.forEach(n=>{let r={...n};if(r.async=this.defaults.async||r.async||!1,n.extensions&&(n.extensions.forEach(i=>{if(!i.name)throw new Error("extension name required");if("renderer"in i){let s=t.renderers[i.name];s?t.renderers[i.name]=function(...o){let a=i.renderer.apply(this,o);return a===!1&&(a=s.apply(this,o)),a}:t.renderers[i.name]=i.renderer}if("tokenizer"in i){if(!i.level||i.level!=="block"&&i.level!=="inline")throw new Error("extension level must be 'block' or 'inline'");let s=t[i.level];s?s.unshift(i.tokenizer):t[i.level]=[i.tokenizer],i.start&&(i.level==="block"?t.startBlock?t.startBlock.push(i.start):t.startBlock=[i.start]:i.level==="inline"&&(t.startInline?t.startInline.push(i.start):t.startInline=[i.start]))}"childTokens"in i&&i.childTokens&&(t.childTokens[i.name]=i.childTokens)}),r.extensions=t),n.renderer){let i=this.defaults.renderer||new P(this.defaults);for(let s in n.renderer){if(!(s in i))throw new Error(`renderer '${s}' does not exist`);if(["options","parser"].includes(s))continue;let o=s,a=n.renderer[o],u=i[o];i[o]=(...p)=>{let c=a.apply(i,p);return c===!1&&(c=u.apply(i,p)),c||""}}r.renderer=i}if(n.tokenizer){let i=this.defaults.tokenizer||new y(this.defaults);for(let s in n.tokenizer){if(!(s in i))throw new Error(`tokenizer '${s}' does not exist`);if(["options","rules","lexer"].includes(s))continue;let o=s,a=n.tokenizer[o],u=i[o];i[o]=(...p)=>{let c=a.apply(i,p);return c===!1&&(c=u.apply(i,p)),c}}r.tokenizer=i}if(n.hooks){let i=this.defaults.hooks||new S;for(let s in n.hooks){if(!(s in i))throw new Error(`hook '${s}' does not exist`);if(["options","block"].includes(s))continue;let o=s,a=n.hooks[o],u=i[o];S.passThroughHooks.has(s)?i[o]=p=>{if(this.defaults.async&&S.passThroughHooksRespectAsync.has(s))return Promise.resolve(a.call(i,p)).then(f=>u.call(i,f));let c=a.call(i,p);return u.call(i,c)}:i[o]=(...p)=>{let c=a.apply(i,p);return c===!1&&(c=u.apply(i,p)),c}}r.hooks=i}if(n.walkTokens){let i=this.defaults.walkTokens,s=n.walkTokens;r.walkTokens=function(o){let a=[];return a.push(s.call(this,o)),i&&(a=a.concat(i.call(this,o))),a}}this.defaults={...this.defaults,...r}}),this}setOptions(e){return this.defaults={...this.defaults,...e},this}lexer(e,t){return b.lex(e,t??this.defaults)}parser(e,t){return R.parse(e,t??this.defaults)}parseMarkdown(e){return(n,r)=>{let i={...r},s={...this.defaults,...i},o=this.onError(!!s.silent,!!s.async);if(this.defaults.async===!0&&i.async===!1)return o(new Error("marked(): The async option was set to true by an extension. Remove async: false from the parse options object to return a Promise."));if(typeof n>"u"||n===n Line truncated
Please report this to https://github.com/markedjs/marked.`,e){let r="<p>An error occurred:</p><pre>"+w(n.message+"",!0)+"</pre>";return t?Promise.resolve(r):r}if(t)return Promise.reject(n);throw n}}};var _=new B;function d(l,e){return _.parse(l,e)}d.options=d.setOptions=function(l){return _.setOptions(l),d.defaults=_.defaults,G(d.defaults),d};d.getDefaults=L;d.defaults=O;d.use=function(...l){return _.use(...l),d.defaults=_.defaults,G(d.defaults),d};d.walkTokens=function(l,e){return _.walkTokens(l,e)};d.parseInline=_.parseInline;d.Parser=R;d.parser=R.parse;d.Renderer=P;d.TextRenderer=$;d.Lexer=b;d.lexer=b.lex;d.Tokenizer=y;d.Hooks=S;d.parse=d;var Dt=d.options,Ht=d.setOptions,Zt=d.use,Gt=d.walkTokens,Nt=d.parseInline,jt=d,Ft=R.parse,Qt=b.lex;export{S as Hooks,b as Lexer,B as Marked,R as Parser,P as Renderer,$ as TextRenderer,y as Tokenizer,O as defaults,L as getDefaults,Qt as lexer,d as marked,Dt as options,jt as parse,Nt as parseInline,Ft as parser,Ht as setOptions,Zt as use,Gt as walkTokens};
@@ -0,0 +1,70 @@
const path = require('path')
module.exports = {
style:
`.md-p {
margin-block-start: 1em;
margin-block-end: 1em;
}
.md-table,
.md-blockquote {
margin-bottom: 16px;
}
.md-table {
box-sizing: border-box;
width: 100%;
overflow: auto;
border-spacing: 0;
border-collapse: collapse;
}
.md-tr {
background-color: #fff;
border-top: 1px solid #c6cbd1;
}
.md-table .md-tr:nth-child(2n) {
background-color: #f6f8fa;
}
.md-th,
.md-td {
padding: 6px 13px !important;
border: 1px solid #dfe2e5;
}
.md-th {
font-weight: 600;
}
.md-blockquote {
padding: 0 1em;
color: #6a737d;
border-left: 0.25em solid #dfe2e5;
}
.md-code {
padding: 0.2em 0.4em;
font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
font-size: 85%;
background-color: rgba(27, 31, 35, 0.05);
border-radius: 3px;
}
.md-pre .md-code {
padding: 0;
font-size: 100%;
background: transparent;
border: 0;
}`,
handler (file) {
// 添加 markdown 属性
if (file.path.includes('miniprogram' + path.sep + 'index.js')) {
file.contents = Buffer.from(file.contents.toString().replace(/properties\s*:\s*{/, 'properties: {\n markdown: Boolean,')
// 处理中文 id
.replace(/navigateTo\s*\(id,\s*offset\)\s*{/, 'navigateTo (id, offset) {\n id = this._ids[decodeURI(id)] || id'))
}
}
}
@@ -0,0 +1,68 @@
module.exports = {
style:
`.md-p {
margin-block-start: 1em;
margin-block-end: 1em;
}
.md-table,
.md-blockquote {
margin-bottom: 16px;
}
.md-table {
box-sizing: border-box;
width: 100%;
overflow: auto;
border-spacing: 0;
border-collapse: collapse;
}
.md-tr {
background-color: #fff;
border-top: 1px solid #c6cbd1;
}
.md-table .md-tr:nth-child(2n) {
background-color: #f6f8fa;
}
.md-th,
.md-td {
padding: 6px 13px !important;
border: 1px solid #dfe2e5;
}
.md-th {
font-weight: 600;
}
.md-blockquote {
padding: 0 1em;
color: #6a737d;
border-left: 0.25em solid #dfe2e5;
}
.md-code {
padding: 0.2em 0.4em;
font-family: SFMono-Regular, Consolas, Liberation Mono, Menlo, monospace;
font-size: 85%;
background-color: rgba(27, 31, 35, 0.05);
border-radius: 3px;
}
.md-pre .md-code {
padding: 0;
font-size: 100%;
background: transparent;
border: 0;
}`,
handler (file) {
// 添加 markdown 属性
if (file.path.includes('mp-html.vue')) {
file.contents = Buffer.from(file.contents.toString().replace(/props\s*:\s*{/, 'props: {\n markdown: Boolean,')
// 处理中文 id
.replace(/navigateTo\s*\(id,\s*offset\)\s*{/, 'navigateTo (id, offset) {\n id = this._ids[decodeURI(id)] || id'))
}
}
}
+46
View File
@@ -0,0 +1,46 @@
# search
功能:关键词搜索
大小:*≈1.5KB*
支持平台:
| 微信小程序 | QQ 小程序 | 百度小程序 | 支付宝小程序 | 头条小程序 | uni-app |
|:---:|:---:|:---:|:---:|:---:|:---:|
| √ | √ | √ | √ | √ | √(nvue 不支持) |
说明:
引入后会在组件实例上挂载一个 *search* 方法,用于关键词搜索
输入值
| 参数名 | 类型 | 默认值 | 说明 |
|:---:|:---:|:---:|---|
| key | String 或 RegExp | - | 要搜索的关键词,支持字符串和正则 |
| anchor | Boolean | false | 是否将搜索结果设置为锚点 |
| style | String | background-color:yellow | 标记搜索结果的样式 |
返回值:*Promise*
| 属性 | 类型 | 说明 |
|:---:|:---:|---|
| num | Number | 搜索结果数量 |
| highlight | Function(i, style='background-color:#FF9632') | 高亮第 i(1 ~ num)个结果,将其样式设置为 style |
| jump | Function(i, offset) | 跳转到第 i(1 ~ num)个结果,偏移量为 offset,anchor 为 true 才可用 |
示例:
```javascript
function search (key) {
// ctx 为组件实例
ctx.search(key, true).then(res => {
res.highlight(1)
res.jump(1, -50) // 高亮第 1 个结果并跳转到该位置,偏移量 -50
})
}
```
附加说明:
1. 不传入 *key*(或为空)时即可取消搜索,取消所有的高亮,还原到原来的效果
2. 进行新的搜索时旧的搜索结果将被还原,旧的结果中的 *highlight* 等方法不再可用
3. 调用 *highlight* 方法高亮一个结果时,之前被高亮的结果会被还原,即始终只有一个结果被高亮
4. *key* 传入字符串时大小写敏感,如果要忽略大小写可以用正则的 *i*(字符串搜索效率高于正则)
5. 设置 *anchor* 为 *true* 会一定程度上降低效率,非必要不要开启
6. 暂不支持跨标签搜索,即只有一个文本节点内包含整个关键词才能被搜索到
@@ -0,0 +1,137 @@
/**
* @fileoverview search 插件
*/
function Search (vm) {
/**
* @description 关键词搜索
* @param {regexp|string} key 要搜索的关键词
* @param {boolean} anchor 是否将搜索结果设置为锚点
* @param {string} style 搜索结果的样式
*/
vm.search = function (key, anchor, style = 'background-color:yellow') {
const obj = {}
const stack = []
const res = [];
// 遍历搜索
(function traversal (nodes, path) {
for (let i = 0; i < nodes.length; i++) {
const node = nodes[i]
if (node.type === 'text' && key) {
const arr = node.text.split(key)
const children = []
if (arr.length > 1) {
// 找到关键词
for (let j = 0; j < arr.length; j++) {
if (arr[j]) {
children.push({
type: 'text',
text: arr[j]
})
}
if (j !== arr.length - 1) {
// 关键词转为一个 span
res.push(`${path}[${i}].children[${children.length}].attrs.style`)
children.push({
name: 'span',
attrs: {
id: anchor ? 'search' + res.length : undefined, // 用于锚点的 id
style
},
children: [{
type: 'text',
text: key instanceof RegExp ? key.exec(node.text)[0] : key
}]
})
}
}
if (key instanceof RegExp) {
key.exec(node.text)
}
if (anchor) {
for (let l = stack.length; l--;) {
// 给父组件做标记,将该标签暴露出来
if (stack[l].c) {
break
} else {
obj[stack[l].path] = 1
}
}
}
obj[`${path}[${i}]`] = {
name: 'span',
c: anchor ? 1 : undefined,
s: 1,
children
}
}
} else if (node.s) {
let text = ''
// 复原上一次的结果
for (let k = 0; k < node.children.length; k++) {
const child = node.children[k]
if (child.text) {
text += child.text
} else {
text += child.children[0].text
}
}
nodes[i] = {
type: 'text',
text
}
if (key && (key instanceof RegExp ? key.test(text) : text.includes(key))) {
i--
} else {
obj[`${path}[${i}]`] = nodes[i]
}
} else if (node.children) {
stack.push({
path: `${path}[${i}].c`,
c: node.c || node.name === 'table'
})
traversal(node.children, `${path}[${i}].children`)
stack.pop()
}
}
})(vm.data.nodes, 'nodes')
return new Promise(function (resolve) {
vm.setData(obj, () => {
resolve({
num: res.length, // 结果数量
/**
* @description 高亮某一个结果
* @param {number} i 第几个
* @param {string} hlstyle 高亮的样式
*/
highlight (i, hlstyle = 'background-color:#FF9632') {
if (i < 1 || i > res.length) return
const obj = {}
if (this.last) {
obj[res[this.last - 1]] = style
}
this.last = i
obj[res[i - 1]] = hlstyle
vm.setData(obj)
},
/**
* @description 跳转到搜索结果
* @param {number} i 第几个
* @param {number} offset 偏移量
*/
jump: anchor
? (i, offset) => {
if (i > 0 && i <= res.length) {
vm.navigateTo('search' + i, offset)
}
}
: undefined
})
})
})
}
}
module.exports = Search
+132
View File
@@ -0,0 +1,132 @@
/**
* @fileoverview search 插件
*/
function Search (vm) {
/**
* @description 关键词搜索
* @param {regexp|string} key 要搜索的关键词
* @param {boolean} anchor 是否将搜索结果设置为锚点
* @param {string} style 搜索结果的样式
*/
vm.search = function (key, anchor, style = 'background-color:yellow') {
const res = []
const stack = [];
// 遍历搜索
(function traversal (nodes) {
for (let i = 0; i < nodes.length; i++) {
let node = nodes[i]
if (node.type === 'text' && key) {
const text = node.text
const arr = text.split(key)
if (arr.length > 1) {
node = {
name: 'span',
attrs: {},
type: 'node',
c: 1,
s: 1,
children: []
}
vm.$set(nodes, i, node)
for (let j = 0; j < arr.length; j++) {
if (arr[j]) {
node.children.push({
type: 'text',
text: arr[j]
})
}
if (j !== arr.length - 1) {
// 关键词转为一个 span
node.children.push({
name: 'span',
attrs: {
id: anchor ? 'search' + (res.length + 1) : undefined, // 用于锚点的 id
style: style
},
// #ifdef VUE3
c: 1,
// #endif
children: [{
type: 'text',
text: key instanceof RegExp ? key.exec(text)[0] : key
}]
})
res.push(node.children[node.children.length - 1].attrs)
}
}
if (key instanceof RegExp) {
key.exec(text)
}
if (anchor) {
for (let l = stack.length; l--;) {
if (stack[l].c) {
break
} else {
vm.$set(stack[l], 'c', 1)
}
}
}
}
} else if (node.s) {
let text = ''
// 复原上一次的结果
for (let k = 0; k < node.children.length; k++) {
const child = node.children[k]
if (child.text) {
text += child.text
} else {
text += child.children[0].text
}
}
vm.$set(nodes, i, {
type: 'text',
text
})
if (key && (key instanceof RegExp ? key.test(text) : text.includes(key))) {
i--
}
} else if (node.children) {
stack.push(node)
traversal(node.children)
stack.pop()
}
}
})(vm.nodes)
return new Promise(function (resolve) {
setTimeout(() => {
resolve({
num: res.length, // 结果数量
/**
* @description 高亮某一个结果
* @param {number} i 第几个
* @param {string} hlstyle 高亮的样式
*/
highlight (i, hlstyle = 'background-color:#FF9632') {
if (i < 1 || i > res.length) return
if (this.last) {
res[this.last - 1].style = style
}
this.last = i
res[i - 1].style = hlstyle
},
/**
* @description 跳转到搜索结果
* @param {number} i 第几个
* @param {number} offset 偏移量
*/
jump: anchor
? (i, offset) => {
if (i > 0 && i <= res.length) {
vm.navigateTo('search' + i, offset)
}
}
: undefined
})
}, 200)
})
}
}
module.exports = Search
+30
View File
@@ -0,0 +1,30 @@
# style
功能:解析和匹配 *style* 标签中的样式
> 这里的 *style* 标签指的是传入 *content* 属性中的 *html* 里包含的 *style* 标签,且 *style* 标签要放在其他标签前面才能生效
大小:*≈3.5KB*
支持平台:
| 微信小程序 | QQ 小程序 | 百度小程序 | 支付宝小程序 | 头条小程序 | uni-app |
|:---:|:---:|:---:|:---:|:---:|:---:|
| √ | √ | √ | √ | √ | √ (nvue 直接支持) |
说明:
支持以下选择器:
| 名称 | 示例 |
|:---:|---|
| 标签名选择器 | p {} |
| class 选择器 | .class {} |
| id 选择器 | #id {} |
| 多选择器交集 | p.class {} |
| 多选择器并集 | p, .class {} |
| 后代选择器 | .class1 .class2 {} |
| 子选择器 | .class1 > .class2 {} |
| 伪类 | .class::before {} |
伪类仅支持 *before* 和 *after*,支持 *attr* 方法
不支持的选择器(属性选择器等)将被忽略
> 由于小程序中无法动态写入 *css*,本插件的实现原理是通过解析,将匹配的样式添加到各标签的行内 *style* 中去,请慎用宽泛的选择器,以免大大增加解析结果大小,减慢渲染速度
+129
View File
@@ -0,0 +1,129 @@
/**
* @fileoverview style 插件
*/
// #ifndef APP-PLUS-NVUE
const Parser = require('./parser')
// #endif
function Style () {
this.styles = []
}
// #ifndef APP-PLUS-NVUE
Style.prototype.onParse = function (node, vm) {
// 获取样式
if (node.name === 'style' && node.children.length && node.children[0].type === 'text') {
this.styles = this.styles.concat(new Parser().parse(node.children[0].text))
} else if (node.name) {
// 匹配样式(对非文本标签)
// 存储不同优先级的样式 name < class < id < 后代
let matched = ['', '', '', '']
for (let i = 0, len = this.styles.length; i < len; i++) {
const item = this.styles[i]
let res = match(node, item.key || item.list[item.list.length - 1])
let j
if (res) {
// 后代选择器
if (!item.key) {
j = item.list.length - 2
for (let k = vm.stack.length; j >= 0 && k--;) {
// 子选择器
if (item.list[j] === '>') {
// 错误情况
if (j < 1 || j > item.list.length - 2) break
if (match(vm.stack[k], item.list[j - 1])) {
j -= 2
} else {
j++
}
} else if (match(vm.stack[k], item.list[j])) {
j--
}
}
res = 4
}
if (item.key || j < 0) {
// 添加伪类
if (item.pseudo && node.children) {
let text
item.style = item.style.replace(/content:([^;]+)/, (_, $1) => {
text = $1.replace(/['"]/g, '')
// 处理 attr 函数
.replace(/attr\((.+?)\)/, (_, $1) => node.attrs[$1.trim()] || '')
// 编码 \xxx
.replace(/\\(\w{4})/, (_, $1) => String.fromCharCode(parseInt($1, 16)))
return ''
})
const pseudo = {
name: 'span',
attrs: {
style: item.style
},
children: [{
type: 'text',
text
}]
}
if (item.pseudo === 'before') {
node.children.unshift(pseudo)
} else {
node.children.push(pseudo)
}
} else {
matched[res - 1] += item.style + (item.style[item.style.length - 1] === ';' ? '' : ';')
}
}
}
}
matched = matched.join('')
if (matched.length > 2) {
node.attrs.style = matched + (node.attrs.style || '')
}
}
}
/**
* @description 匹配样式
* @param {object} node 要匹配的标签
* @param {string|string[]} keys 选择器
* @returns {number} 0:不匹配;1:name 匹配;2:class 匹配;3:id 匹配
*/
function match (node, keys) {
function matchItem (key) {
if (key[0] === '#') {
// 匹配 id
if (node.attrs.id && node.attrs.id.trim() === key.substr(1)) return 3
} else if (key[0] === '.') {
// 匹配 class
key = key.substr(1)
const selectors = (node.attrs.class || '').split(' ')
for (let i = 0; i < selectors.length; i++) {
if (selectors[i].trim() === key) return 2
}
} else if (node.name === key) {
// 匹配 name
return 1
}
return 0
}
// 多选择器交集
if (keys instanceof Array) {
let res = 0
for (let j = 0; j < keys.length; j++) {
const tmp = matchItem(keys[j])
// 任意一个不匹配就失败
if (!tmp) return 0
// 优先级最大的一个作为最终优先级
if (tmp > res) {
res = tmp
}
}
return res
}
return matchItem(keys)
}
// #endif
module.exports = Style
+175
View File
@@ -0,0 +1,175 @@
const blank = {
' ': true,
'\n': true,
'\t': true,
'\r': true,
'\f': true
}
function Parser () {
this.styles = []
this.selectors = []
}
/**
* @description 解析 css 字符串
* @param {string} content css 内容
*/
Parser.prototype.parse = function (content) {
new Lexer(this).parse(content)
return this.styles
}
/**
* @description 解析到一个选择器
* @param {string} name 名称
*/
Parser.prototype.onSelector = function (name) {
// 不支持的选择器
if (name.includes('[') || name.includes('*') || name.includes('@')) return
const selector = {}
// 伪类
if (name.includes(':')) {
const info = name.split(':')
const pseudo = info.pop()
if (pseudo === 'before' || pseudo === 'after') {
selector.pseudo = pseudo
name = info[0]
} else return
}
// 分割交集选择器
function splitItem (str) {
const arr = []
let i, start
for (i = 1, start = 0; i < str.length; i++) {
if (str[i] === '.' || str[i] === '#') {
arr.push(str.substring(start, i))
start = i
}
}
if (!arr.length) {
return str
} else {
arr.push(str.substring(start, i))
return arr
}
}
// 后代选择器
if (name.includes(' ')) {
selector.list = []
const list = name.split(' ')
for (let i = 0; i < list.length; i++) {
if (list[i].length) {
// 拆分子选择器
const arr = list[i].split('>')
for (let j = 0; j < arr.length; j++) {
selector.list.push(splitItem(arr[j]))
if (j < arr.length - 1) {
selector.list.push('>')
}
}
}
}
} else {
selector.key = splitItem(name)
}
this.selectors.push(selector)
}
/**
* @description 解析到选择器内容
* @param {string} content 内容
*/
Parser.prototype.onContent = function (content) {
// 并集选择器
for (let i = 0; i < this.selectors.length; i++) {
this.selectors[i].style = content
}
this.styles = this.styles.concat(this.selectors)
this.selectors = []
}
/**
* @description css 词法分析器
* @param {object} handler 高层处理器
*/
function Lexer (handler) {
this.selector = ''
this.style = ''
this.handler = handler
}
Lexer.prototype.parse = function (content) {
this.i = 0
this.content = content
this.state = this.blank
for (let len = content.length; this.i < len; this.i++) {
this.state(content[this.i])
}
}
Lexer.prototype.comment = function () {
this.i = this.content.indexOf('*/', this.i) + 1
if (!this.i) {
this.i = this.content.length
}
}
Lexer.prototype.blank = function (c) {
if (!blank[c]) {
if (c === '/' && this.content[this.i + 1] === '*') {
this.comment()
return
}
this.selector += c
this.state = this.name
}
}
Lexer.prototype.name = function (c) {
if (c === '/' && this.content[this.i + 1] === '*') {
this.comment()
return
}
if (c === '{' || c === ',' || c === ';') {
this.handler.onSelector(this.selector.trimEnd())
this.selector = ''
if (c !== '{') {
while (blank[this.content[++this.i]]);
}
if (this.content[this.i] === '{') {
this.floor = 1
this.state = this.val
} else {
this.selector += this.content[this.i]
}
} else if (blank[c]) {
this.selector += ' '
} else {
this.selector += c
}
}
Lexer.prototype.val = function (c) {
if (c === '/' && this.content[this.i + 1] === '*') {
this.comment()
return
}
if (c === '{') {
this.floor++
} else if (c === '}') {
this.floor--
if (!this.floor) {
this.handler.onContent(this.style)
this.style = ''
this.state = this.blank
return
}
}
this.style += c
}
module.exports = Parser
+2
View File
@@ -0,0 +1,2 @@
# 模板
这是一个插件模板,需要开发插件时,可以从这个模板开始
+65
View File
@@ -0,0 +1,65 @@
/**
* @description 插件构建文件模板
*/
module.exports = {
/**
* @description 入口文件
* @type {String}
* @default 'index.js'
*/
main: 'index.js',
/**
* @description 支持的平台
* @type {String[]}
* @default ['mp-weixin','mp-qq','mp-baidu','mp-alipay','mp-toutiao','uni-app']
*/
platform: ['mp-weixin', 'mp-qq', 'mp-baidu', 'mp-alipay', 'mp-toutiao', 'uni-app'],
/**
* @description 要被添加到模板文件中的标签(将被添加到 src/node/node.wxml)
* 必须要有 wx:if 表明什么情况下使用该标签
* n 表示标签结构体,<node> 标签用于递归显示子节点(可参考源文件中的写法)
* @type {String}
*/
template: '',
/**
* @description 用于处理模板中事件的方法(将被添加到 src/node/node.js)
* 需要触发顶层组件的事件请使用 this.root.triggerEvent
* @type {Object}
*/
methods: {
},
/**
* @description 用于模板文件的 css 样式(将被添加到 src/node/node.wxss)
* @type {String}
*/
style: '',
/**
* @description 要被引入到模板文件的 css 文件路径(将被添加到 src/node/node.wxss)
* @type {String|String[]}
*/
import: [],
/**
* @description 在模板中需要使用的组件或插件列表(将被添加到 src/node/node.json)
* @type {Object}
*/
usingComponents: {
},
/**
* @description 自定义文件处理器
* 如果上述处理还无法满足要求,可以在此方法中进行处理
* 所有 src 目录下的文件和本插件目录下的文件都会经过此方法的处理
* @param {Vinyl} file 关于该文件对象的格式可参考 https://github.com/gulpjs/vinyl#instance-methods
* @param {String} platform 平台
*/
handler (file, platform) {
let content = file.contents.toString()
// 进行处理
if (platform === 'xxx') {
content = content.replace('aaa', 'bbb')
}
file.contents = Buffer.from(content)
}
}
+67
View File
@@ -0,0 +1,67 @@
/**
* @fileoverview 插件入口文件模板
*/
const data = {} // 全局数据
/**
* @description 组件被创建时将实例化插件
* @param {Component} vm 组件实例
*/
function Plugin (vm) {
this.vm = vm // 保存实例在其他周期使用
this.compData = {} // 仅在单个组件中使用的数据
data.xxx = 'xxx' // 记录全局数据
}
/**
* @description html 数据更新时触发
* @param {string} content 要更新的 html 字符串
* @param {object} config 解析配置
* @returns {string|void} 如果要对 html 字符串进行一些预处理,则返回处理后的字符串
*/
Plugin.prototype.onUpdate = function (content, config) {
config.ignoreTags.xxx = true // 移除 xxx 标签
// 对 html 内容进行预处理并返回修改,没有修改则不需要返回
return content
}
/**
* @description 解析到一个标签时触发
* @param {object} node 标签
* @param {object} parser 解析器实例
* @returns {boolean|void} 如果返回 false 将移除该标签
*/
Plugin.prototype.onParse = function (node, parser) {
// 处理文本标签
if (node.type === 'text') {
// node.text 文本内容
} else {
// 处理元素标签
// node.name 标签名
// node.attrs 属性列表
// node.children 子节点(非自闭合标签有)
if (node.name === 'xxx') {
parser.expose() // 如果该标签不能被 rich-text 包含,需要调用此方法暴露出来
// parser.options 组件传入的一些解析属性
// parser.stack 可以从栈中获取祖先节点
}
}
}
/**
* @description dom 树加载完毕时触发(load 事件)
*/
Plugin.prototype.onLoad = function () {
// 可以获取媒体 context 对象等
}
/**
* @description 组件被移除时触发
*/
Plugin.prototype.onDetached = function () {
// 可以释放一些必要的资源(计时器等)
}
module.exports = Plugin
+18
View File
@@ -0,0 +1,18 @@
# txv-video
功能:使用腾讯视频
大小:*≈1KB*
支持平台:
| 微信小程序 | QQ 小程序 | 百度小程序 | 支付宝小程序 | 头条小程序 | uni-app |
|:---:|:---:|:---:|:---:|:---:|:---:|
| √ | √ | | | | √ (h5 和 app 直接支持) |
说明:
引入本插件后,*html* 中符合下方格式的 *iframe* 标签(*src* 中含有 *vid*)将被转为通过腾讯视频播放:
```html
<iframe src="https://v.qq.com/txp/iframe/player.html?vid=xxxxxx" allowFullScreen="true"></iframe>
```
同时,其可以被 *pause-video* 属性控制
!> 本插件仅用于将官方 [腾讯视频插件](https://github.com/tvfe/txv-miniprogram-plugin) 应用于本组件,仅在微信和 *qq* 平台有效,使用前请确认已经成功申请使用该插件并按要求在小程序 *app.json* 中配置完成,否则可能报错 **This application has not registered any plugins yet** 且无法生效
+3
View File
@@ -0,0 +1,3 @@
module.exports = {
platform: ['mp-weixin', 'mp-qq', 'uni-app']
}
+46
View File
@@ -0,0 +1,46 @@
/**
* @fileoverview txv-video 插件
* Include txv-video (https://github.com/tvfe/txv-miniprogram-plugin)
*/
const TxvVideo = function (vm) {
this.vm = vm
}
// #ifdef MP-WEIXIN || MP-QQ
try {
const TxvContext = requirePlugin('tencentvideo')
TxvVideo.prototype.onLoad = function () {
setTimeout(() => {
for (let i = 0; i < this.videos.length; i++) {
const ctx = TxvContext.getTxvContext(this.videos[i])
ctx.id = this.videos[i]
this.vm._videos.push(ctx)
}
}, 50)
}
} catch (e) {
console.error('使用txv-video扩展需注册腾讯视频插件')
}
TxvVideo.prototype.onUpdate = function (_, config) {
config.trustTags['txv-video'] = true
this.videos = []
}
TxvVideo.prototype.onParse = function (node, parser) {
if (node.name === 'iframe' && (node.attrs.src || '').includes('vid')) {
const vid = node.attrs.src.match(/vid=([^&\s]+)/)
if (vid) {
node.name = 'txv-video'
node.attrs.vid = vid[1]
this.videos.push(vid[1])
node.attrs.src = undefined
parser.expose()
}
}
}
// #endif
module.exports = TxvVideo
@@ -0,0 +1,6 @@
module.exports = {
template: '<txv-video wx:if="{{n.name==\'txv-video\'}}" vid="{{n.attrs.vid}}" playerid="{{n.attrs.vid}}" id="{{n.attrs.vid}}" class="{{n.attrs.class}}" style="{{n.attrs.style}}" controls data-i="{{i}}" bindplay="play" binderror="mediaError" />',
usingComponents: {
'txv-video': 'plugin://tencentvideo/video'
}
}
@@ -0,0 +1,3 @@
module.exports = {
template: '<txv-video v-if="n.name==\'txv-video\'" :vid="n.attrs.vid" :playerid="n.attrs.vid" :id="n.attrs.vid" :class="n.attrs.class" :style="n.attrs.style" controls :data-i="i" @play="play" @error="mediaError" />'
}