Repository: Honye/weapp-mark Branch: main Commit: 881f3651f209 Files: 513 Total size: 1.9 MB Directory structure: gitextract_1qcrx63f/ ├── .eslintignore ├── .eslintrc.cjs ├── .github/ │ └── workflows/ │ └── upload.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── .vscode/ │ └── settings.json ├── .yarnrc ├── @types/ │ ├── douban/ │ │ ├── accounts.d.ts │ │ ├── frodo.d.ts │ │ └── index.d.ts │ ├── douban.d.ts │ ├── index.d.ts │ ├── miniprogram.d.ts │ ├── mobx-miniprogram-bindings.d.ts │ └── wxCloud.d.ts ├── CODE_OF_CONDUCT.md ├── LICENSE ├── README.md ├── cloudbaserc.json ├── cloudfunctions/ │ ├── app/ │ │ ├── config.json │ │ ├── index.js │ │ └── package.json │ ├── douban/ │ │ ├── config.json │ │ ├── index.js │ │ ├── package.json │ │ ├── request.d.ts │ │ └── request.js │ ├── favArticle/ │ │ ├── index.js │ │ └── package.json │ ├── favCard/ │ │ ├── index.js │ │ └── package.json │ ├── fetch/ │ │ ├── config.json │ │ ├── index.js │ │ └── package.json │ ├── getArticleDetails/ │ │ ├── index.js │ │ └── package.json │ ├── getCard/ │ │ ├── config.json │ │ ├── index.js │ │ └── package.json │ ├── getCards/ │ │ ├── index.js │ │ └── package.json │ ├── getCategories/ │ │ ├── index.js │ │ └── package.json │ ├── getFavArticles/ │ │ ├── index.js │ │ └── package.json │ ├── getFavCards/ │ │ ├── index.js │ │ └── package.json │ ├── github/ │ │ ├── config.json │ │ ├── fetch.js │ │ ├── index.js │ │ └── package.json │ ├── initdb/ │ │ ├── index.js │ │ └── package.json │ ├── login/ │ │ ├── config.json │ │ ├── index.js │ │ └── package.json │ ├── nowPlaying/ │ │ ├── config.json │ │ ├── fetch.js │ │ ├── index.js │ │ └── package.json │ ├── showingSoon/ │ │ ├── config.json │ │ ├── index.js │ │ └── package.json │ ├── site/ │ │ ├── config.json │ │ ├── index.js │ │ └── package.json │ ├── subscribeMessage/ │ │ ├── index.js │ │ └── package.json │ ├── subscription/ │ │ ├── config.json │ │ ├── index.js │ │ └── package.json │ ├── trending/ │ │ ├── config.json │ │ ├── index.js │ │ └── package.json │ ├── wallpaper/ │ │ ├── index.js │ │ └── package.json │ └── wxacode/ │ ├── config.json │ ├── index.js │ └── package.json ├── docs/ │ └── 云开发环境初始化.md ├── jest.config.js ├── miniprogram/ │ ├── apis/ │ │ ├── cloud/ │ │ │ └── index.js │ │ ├── douban/ │ │ │ ├── accounts.js │ │ │ └── request.js │ │ ├── douban.js │ │ ├── github.js │ │ ├── leancloud/ │ │ │ └── index.js │ │ ├── server/ │ │ │ └── index.js │ │ └── vercel.js │ ├── app.js │ ├── app.json │ ├── app.wxss │ ├── components/ │ │ ├── Tabs/ │ │ │ ├── Tabs.js │ │ │ ├── Tabs.json │ │ │ ├── Tabs.wxml │ │ │ └── Tabs.wxss │ │ ├── article/ │ │ │ ├── article.js │ │ │ ├── article.json │ │ │ ├── article.wxml │ │ │ └── article.wxss │ │ ├── btn-fav/ │ │ │ ├── FavButton.js │ │ │ ├── FavButton.json │ │ │ ├── FavButton.wxml │ │ │ └── FavButton.wxss │ │ ├── cover-page/ │ │ │ ├── cover-page.js │ │ │ ├── cover-page.json │ │ │ ├── cover-page.wxml │ │ │ ├── cover-page.wxs │ │ │ └── cover-page.wxss │ │ ├── index-list/ │ │ │ ├── components/ │ │ │ │ └── content/ │ │ │ │ ├── content.js │ │ │ │ ├── content.json │ │ │ │ ├── content.wxml │ │ │ │ └── content.wxss │ │ │ ├── index-list.js │ │ │ ├── index-list.json │ │ │ ├── index-list.wxml │ │ │ └── index-list.wxss │ │ ├── load-more/ │ │ │ ├── load-more.js │ │ │ ├── load-more.json │ │ │ ├── load-more.wxml │ │ │ └── load-more.wxss │ │ ├── painter/ │ │ │ ├── lib/ │ │ │ │ ├── downloader.js │ │ │ │ ├── gradient.js │ │ │ │ ├── pen.js │ │ │ │ ├── qrcode.js │ │ │ │ └── util.js │ │ │ ├── painter.js │ │ │ ├── painter.json │ │ │ └── painter.wxml │ │ ├── pre-image/ │ │ │ ├── PreImage.js │ │ │ ├── PreImage.json │ │ │ ├── PreImage.wxml │ │ │ └── PreImage.wxss │ │ ├── rating/ │ │ │ ├── rating.js │ │ │ ├── rating.json │ │ │ ├── rating.wxml │ │ │ └── rating.wxss │ │ ├── tab-bar/ │ │ │ ├── index.js │ │ │ ├── index.json │ │ │ ├── index.wxml │ │ │ └── index.wxss │ │ └── towxml/ │ │ ├── audio-player/ │ │ │ ├── Audio.js │ │ │ ├── audio-player.js │ │ │ ├── audio-player.json │ │ │ ├── audio-player.wxml │ │ │ └── audio-player.wxss │ │ ├── config.js │ │ ├── decode.js │ │ ├── decode.json │ │ ├── decode.wxml │ │ ├── decode.wxss │ │ ├── img/ │ │ │ ├── img.js │ │ │ ├── img.json │ │ │ ├── img.wxml │ │ │ └── img.wxss │ │ ├── index.js │ │ ├── parse/ │ │ │ ├── highlight/ │ │ │ │ ├── highlight.js │ │ │ │ ├── index.js │ │ │ │ ├── languages/ │ │ │ │ │ ├── bash.js │ │ │ │ │ ├── c-like.js │ │ │ │ │ ├── c.js │ │ │ │ │ ├── css.js │ │ │ │ │ ├── dart.js │ │ │ │ │ ├── go.js │ │ │ │ │ ├── htmlbars.js │ │ │ │ │ ├── index.js │ │ │ │ │ ├── java.js │ │ │ │ │ ├── javascript.js │ │ │ │ │ ├── json.js │ │ │ │ │ ├── less.js │ │ │ │ │ ├── nginx.js │ │ │ │ │ ├── php.js │ │ │ │ │ ├── python-repl.js │ │ │ │ │ ├── python.js │ │ │ │ │ ├── scss.js │ │ │ │ │ ├── shell.js │ │ │ │ │ ├── typescript.js │ │ │ │ │ └── xml.js │ │ │ │ └── style/ │ │ │ │ ├── github.wxss │ │ │ │ └── monokai.wxss │ │ │ ├── index.js │ │ │ ├── markdown/ │ │ │ │ ├── index.js │ │ │ │ ├── markdown.js │ │ │ │ └── plugins/ │ │ │ │ ├── emoji.js │ │ │ │ ├── ins.js │ │ │ │ ├── mark.js │ │ │ │ ├── sub.js │ │ │ │ ├── sup.js │ │ │ │ └── todo.js │ │ │ └── parse2/ │ │ │ ├── Parser.js │ │ │ ├── Tokenizer.js │ │ │ ├── domhandler/ │ │ │ │ ├── index.js │ │ │ │ └── node.js │ │ │ ├── entities/ │ │ │ │ ├── decode.js │ │ │ │ ├── decode_codepoint.js │ │ │ │ ├── encode.js │ │ │ │ ├── index.js │ │ │ │ └── maps/ │ │ │ │ ├── decode.js │ │ │ │ ├── entities.js │ │ │ │ ├── legacy.js │ │ │ │ └── xml.js │ │ │ └── index.js │ │ ├── style/ │ │ │ ├── main.wxss │ │ │ └── theme/ │ │ │ ├── dark.wxss │ │ │ └── light.wxss │ │ ├── table/ │ │ │ ├── table.js │ │ │ ├── table.json │ │ │ ├── table.wxml │ │ │ └── table.wxss │ │ ├── towxml.js │ │ ├── towxml.json │ │ ├── towxml.wxml │ │ └── towxml.wxss │ ├── libs/ │ │ ├── av-live-query-core-min.js │ │ └── leancloud-adapters-weapp.js │ ├── models/ │ │ ├── Cast.js │ │ └── Comment.js │ ├── packages/ │ │ ├── admin/ │ │ │ └── pages/ │ │ │ ├── app/ │ │ │ │ ├── app.js │ │ │ │ ├── app.json │ │ │ │ ├── app.wxml │ │ │ │ └── app.wxss │ │ │ ├── douban/ │ │ │ │ ├── douban.js │ │ │ │ ├── douban.json │ │ │ │ ├── douban.wxml │ │ │ │ └── douban.wxss │ │ │ └── index/ │ │ │ ├── index.js │ │ │ ├── index.json │ │ │ ├── index.wxml │ │ │ └── index.wxss │ │ ├── article/ │ │ │ └── pages/ │ │ │ ├── categories/ │ │ │ │ ├── categories.js │ │ │ │ ├── categories.json │ │ │ │ ├── categories.wxml │ │ │ │ └── categories.wxss │ │ │ ├── classification/ │ │ │ │ ├── index.wxml │ │ │ │ └── index.wxss │ │ │ ├── details/ │ │ │ │ ├── detail.js │ │ │ │ ├── detail.json │ │ │ │ ├── detail.wxml │ │ │ │ └── detail.wxss │ │ │ └── movie-list-detail/ │ │ │ ├── movie-list-detail.js │ │ │ ├── movie-list-detail.json │ │ │ ├── movie-list-detail.wxml │ │ │ └── movie-list-detail.wxss │ │ ├── douban/ │ │ │ └── pages/ │ │ │ ├── collection/ │ │ │ │ ├── collection.js │ │ │ │ ├── collection.json │ │ │ │ ├── collection.wxml │ │ │ │ └── collection.wxss │ │ │ └── login-phone/ │ │ │ ├── login-phone.js │ │ │ ├── login-phone.json │ │ │ ├── login-phone.wxml │ │ │ └── login-phone.wxss │ │ ├── example/ │ │ │ └── pages/ │ │ │ ├── cover-page/ │ │ │ │ ├── cover-page.js │ │ │ │ ├── cover-page.json │ │ │ │ ├── cover-page.wxml │ │ │ │ └── cover-page.wxss │ │ │ ├── icons/ │ │ │ │ ├── icons.js │ │ │ │ ├── icons.json │ │ │ │ ├── icons.wxml │ │ │ │ └── icons.wxss │ │ │ ├── index/ │ │ │ │ ├── index.js │ │ │ │ ├── index.json │ │ │ │ ├── index.wxml │ │ │ │ └── index.wxss │ │ │ ├── tab-bar/ │ │ │ │ ├── tab-bar.js │ │ │ │ ├── tab-bar.json │ │ │ │ ├── tab-bar.wxml │ │ │ │ └── tab-bar.wxss │ │ │ └── waterfall/ │ │ │ ├── components/ │ │ │ │ └── unsplash/ │ │ │ │ ├── unsplash.js │ │ │ │ ├── unsplash.json │ │ │ │ ├── unsplash.wxml │ │ │ │ └── unsplash.wxss │ │ │ ├── waterfall.js │ │ │ ├── waterfall.json │ │ │ ├── waterfall.wxml │ │ │ └── waterfall.wxss │ │ ├── github/ │ │ │ ├── components/ │ │ │ │ ├── event-item/ │ │ │ │ │ ├── event-item.js │ │ │ │ │ ├── event-item.json │ │ │ │ │ ├── event-item.wxml │ │ │ │ │ ├── event-item.wxs │ │ │ │ │ └── event-item.wxss │ │ │ │ ├── lang-item/ │ │ │ │ │ ├── lang-item.js │ │ │ │ │ ├── lang-item.json │ │ │ │ │ ├── lang-item.wxml │ │ │ │ │ └── lang-item.wxss │ │ │ │ ├── notification-item/ │ │ │ │ │ ├── notification-item.js │ │ │ │ │ ├── notification-item.json │ │ │ │ │ ├── notification-item.wxml │ │ │ │ │ └── notification-item.wxss │ │ │ │ ├── page-events/ │ │ │ │ │ ├── page-events.js │ │ │ │ │ ├── page-events.json │ │ │ │ │ ├── page-events.wxml │ │ │ │ │ └── page-events.wxss │ │ │ │ ├── page-trending/ │ │ │ │ │ ├── page-trending.js │ │ │ │ │ ├── page-trending.json │ │ │ │ │ ├── page-trending.wxml │ │ │ │ │ └── page-trending.wxss │ │ │ │ ├── page-user/ │ │ │ │ │ ├── page-user.js │ │ │ │ │ ├── page-user.json │ │ │ │ │ ├── page-user.wxml │ │ │ │ │ └── page-user.wxss │ │ │ │ └── repo-item/ │ │ │ │ ├── repo-item.js │ │ │ │ ├── repo-item.json │ │ │ │ ├── repo-item.wxml │ │ │ │ └── repo-item.wxss │ │ │ └── pages/ │ │ │ ├── events/ │ │ │ │ ├── events.js │ │ │ │ ├── events.json │ │ │ │ ├── events.wxml │ │ │ │ └── events.wxss │ │ │ ├── home/ │ │ │ │ ├── home.js │ │ │ │ ├── home.json │ │ │ │ ├── home.wxml │ │ │ │ └── home.wxss │ │ │ ├── languages/ │ │ │ │ ├── languages.js │ │ │ │ ├── languages.json │ │ │ │ ├── languages.wxml │ │ │ │ └── languages.wxss │ │ │ ├── notifications/ │ │ │ │ ├── notifications.js │ │ │ │ ├── notifications.json │ │ │ │ ├── notifications.wxml │ │ │ │ └── notifications.wxss │ │ │ ├── repository/ │ │ │ │ ├── repository.js │ │ │ │ ├── repository.json │ │ │ │ ├── repository.wxml │ │ │ │ └── repository.wxss │ │ │ ├── search/ │ │ │ │ ├── search.js │ │ │ │ ├── search.json │ │ │ │ ├── search.wxml │ │ │ │ └── search.wxss │ │ │ ├── starred/ │ │ │ │ ├── starred.js │ │ │ │ ├── starred.json │ │ │ │ ├── starred.wxml │ │ │ │ └── starred.wxss │ │ │ └── trending/ │ │ │ ├── trending.js │ │ │ ├── trending.json │ │ │ ├── trending.wxml │ │ │ └── trending.wxss │ │ ├── movie/ │ │ │ ├── components/ │ │ │ │ └── comment-item/ │ │ │ │ ├── comment-item.js │ │ │ │ ├── comment-item.json │ │ │ │ ├── comment-item.wxml │ │ │ │ └── comment-item.wxss │ │ │ └── pages/ │ │ │ ├── cards/ │ │ │ │ ├── card.js │ │ │ │ ├── card.json │ │ │ │ ├── card.wxml │ │ │ │ └── card.wxss │ │ │ ├── details/ │ │ │ │ ├── details.js │ │ │ │ ├── details.json │ │ │ │ ├── details.wxml │ │ │ │ ├── details.wxs │ │ │ │ └── details.wxss │ │ │ ├── intheaters/ │ │ │ │ ├── in_theaters.js │ │ │ │ ├── in_theaters.json │ │ │ │ ├── in_theaters.wxml │ │ │ │ └── in_theaters.wxss │ │ │ ├── mark/ │ │ │ │ ├── mark.js │ │ │ │ ├── mark.json │ │ │ │ ├── mark.wxml │ │ │ │ └── mark.wxss │ │ │ ├── photos/ │ │ │ │ ├── photos.js │ │ │ │ ├── photos.json │ │ │ │ ├── photos.wxml │ │ │ │ └── photos.wxss │ │ │ └── trailers/ │ │ │ ├── trailers.js │ │ │ ├── trailers.json │ │ │ ├── trailers.wxml │ │ │ └── trailers.wxss │ │ ├── tools/ │ │ │ └── pages/ │ │ │ ├── encode/ │ │ │ │ ├── encode.js │ │ │ │ ├── encode.json │ │ │ │ ├── encode.wxml │ │ │ │ └── encode.wxss │ │ │ ├── index/ │ │ │ │ ├── index.js │ │ │ │ ├── index.json │ │ │ │ ├── index.wxml │ │ │ │ └── index.wxss │ │ │ └── random/ │ │ │ ├── random.js │ │ │ ├── random.json │ │ │ ├── random.wxml │ │ │ └── random.wxss │ │ ├── user/ │ │ │ └── pages/ │ │ │ ├── achievement/ │ │ │ │ ├── achievement.js │ │ │ │ ├── achievement.json │ │ │ │ ├── achievement.wxml │ │ │ │ └── achievement.wxss │ │ │ ├── evaluate/ │ │ │ │ ├── evaluate.js │ │ │ │ ├── evaluate.json │ │ │ │ ├── evaluate.wxml │ │ │ │ └── evaluate.wxss │ │ │ ├── favCards/ │ │ │ │ ├── index.js │ │ │ │ ├── index.json │ │ │ │ ├── index.wxml │ │ │ │ └── index.wxss │ │ │ ├── favMovieList/ │ │ │ │ ├── index.js │ │ │ │ ├── index.json │ │ │ │ ├── index.wxml │ │ │ │ └── index.wxss │ │ │ └── userinfo/ │ │ │ ├── userinfo.js │ │ │ ├── userinfo.json │ │ │ ├── userinfo.wxml │ │ │ └── userinfo.wxss │ │ └── wallpaper/ │ │ ├── apis/ │ │ │ └── wallpaper/ │ │ │ └── index.js │ │ └── pages/ │ │ └── categories/ │ │ ├── categories.js │ │ ├── categories.json │ │ ├── categories.wxml │ │ └── categories.wxss │ ├── pages/ │ │ ├── about/ │ │ │ ├── about.js │ │ │ ├── about.json │ │ │ ├── about.wxml │ │ │ └── about.wxss │ │ ├── first/ │ │ │ ├── first.js │ │ │ ├── first.json │ │ │ ├── first.wxml │ │ │ └── first.wxss │ │ ├── marked/ │ │ │ ├── marked.js │ │ │ ├── marked.json │ │ │ ├── marked.wxml │ │ │ └── marked.wxss │ │ ├── search/ │ │ │ ├── search.js │ │ │ ├── search.json │ │ │ ├── search.wxml │ │ │ └── search.wxss │ │ ├── setting/ │ │ │ ├── setting.js │ │ │ ├── setting.json │ │ │ ├── setting.wxml │ │ │ └── setting.wxss │ │ ├── splash/ │ │ │ ├── splash.js │ │ │ ├── splash.json │ │ │ ├── splash.wxml │ │ │ └── splash.wxss │ │ ├── tabs/ │ │ │ ├── discovery/ │ │ │ │ ├── discovery.js │ │ │ │ ├── discovery.json │ │ │ │ ├── discovery.wxml │ │ │ │ └── discovery.wxss │ │ │ ├── index/ │ │ │ │ ├── index.js │ │ │ │ ├── index.json │ │ │ │ ├── index.wxml │ │ │ │ └── index.wxss │ │ │ └── movies/ │ │ │ ├── movies.js │ │ │ ├── movies.json │ │ │ ├── movies.wxml │ │ │ └── movies.wxss │ │ ├── test/ │ │ │ ├── test.js │ │ │ ├── test.json │ │ │ ├── test.wxml │ │ │ └── test.wxss │ │ └── webview/ │ │ ├── index.js │ │ ├── index.json │ │ ├── index.wxml │ │ └── index.wxss │ ├── sitemap.json │ ├── store/ │ │ ├── app.js │ │ ├── douban.js │ │ ├── index.js │ │ └── user.js │ ├── style/ │ │ ├── animate.wxss │ │ ├── common.wxss │ │ ├── font-awesome.min.wxss │ │ ├── iconfont.wxss │ │ └── weui.wxss │ ├── templates/ │ │ ├── actionsheet/ │ │ │ ├── actionsheet.js │ │ │ ├── actionsheet.wxml │ │ │ └── actionsheet.wxss │ │ ├── bing/ │ │ │ ├── bing.js │ │ │ ├── bing.wxml │ │ │ └── bing.wxss │ │ ├── casts/ │ │ │ ├── casts.wxml │ │ │ └── casts.wxss │ │ ├── cell/ │ │ │ ├── cell.wxml │ │ │ └── cell.wxss │ │ ├── circle/ │ │ │ ├── circle.wxml │ │ │ └── circle.wxss │ │ ├── component.js │ │ ├── dropmenu/ │ │ │ ├── dropmenu.js │ │ │ ├── dropmenu.wxml │ │ │ └── dropmenu.wxss │ │ ├── index.js │ │ ├── loading/ │ │ │ ├── loading.wxml │ │ │ └── loading.wxss │ │ ├── login/ │ │ │ ├── login.js │ │ │ └── login.wxml │ │ ├── movie/ │ │ │ ├── movieRow.wxml │ │ │ └── movieRow.wxss │ │ ├── rating/ │ │ │ ├── rating.wxml │ │ │ └── rating.wxss │ │ ├── share/ │ │ │ ├── share.js │ │ │ ├── share.wxml │ │ │ └── share.wxss │ │ └── wxParse/ │ │ ├── html2json.js │ │ ├── htmlparser.js │ │ ├── showdown.js │ │ ├── wxDiscode.js │ │ ├── wxParse.js │ │ ├── wxParse.wxml │ │ └── wxParse.wxss │ └── utils/ │ ├── Base64.js │ ├── EventEmitter.js │ ├── URLSearchParams.js │ ├── WxUtil.wxs │ ├── apis.js │ ├── crypro.js │ ├── events.js │ ├── github-colors.js │ ├── global.js │ ├── request.js │ ├── storage.js │ ├── svg.js │ ├── util.js │ ├── utils.wxs │ └── wxCloud.js ├── mock/ │ └── mock.config.json ├── package.json ├── project.config.json ├── scripts/ │ └── release.cjs ├── test/ │ └── EventEmitter.spec.js └── tsconfig.json ================================================ FILE CONTENTS ================================================ ================================================ FILE: .eslintignore ================================================ **/node_modules miniprogram/templates/wxParse/ ================================================ FILE: .eslintrc.cjs ================================================ module.exports = { "env": { "browser": true, "es6": true, "node": true }, "extends": [ "eslint:recommended", "plugin:@typescript-eslint/recommended", // "prettier" put last "prettier", ], "globals": { "Atomics": "readonly", "SharedArrayBuffer": "readonly", "wx": "writable", "App": "readonly", "Page": "readonly", "Component": "readonly", "getApp": "readonly", "getCurrentPages": "readonly" }, "parser": "@typescript-eslint/parser", "parserOptions": { "ecmaVersion": 2018, "sourceType": "module" }, "plugins": [ "@typescript-eslint" ], "rules": { "@typescript-eslint/no-unused-vars": ["error", { "args": "none" }], "comma-dangle": ["error", "only-multiline"] }, "overrides": [ { "files": ["*.wxs"], "globals": { "getDate": "readonly", "getRegExp": "readonly" }, "env": { "es6": false }, "parserOptions": { "ecmaVersion": 5, "sourceType": "script" }, "rules": { "quote-props": ["error", "as-needed", { "numbers": true }], "no-useless-escape": "off" } }, { "files": ["./cloudfunctions/**/*.js"], "rules": { "@typescript-eslint/no-var-requires": "off" } }, { "files": ["*.js"], "rules": { "@typescript-eslint/explicit-module-boundary-types": "off", "@typescript-eslint/explicit-function-return-type": "off" } }, { files: ['./scripts/**/*.cjs'], rules: { '@typescript-eslint/no-var-requires': 'off', }, }, { files: ['**/*.spec.{js,mjs}'], env: { jest: true }, }, ] }; ================================================ FILE: .github/workflows/upload.yml ================================================ # 构建并上传到小程序后台 name: Upload to weixin # Controls when the workflow will run on: # Triggers the workflow on push but only for the main branch push: branches: - main # Allows you to run this workflow manually from the Actions tab workflow_dispatch: jobs: build: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - uses: actions/setup-node@v3 with: node-version: 16 - name: Create file "private.key" env: WX_PRIVATE_KEY: ${{ secrets.WX_PRIVATE_KEY }} run: | echo "$WX_PRIVATE_KEY" > private.key - name: Install dependencies run: | corepack enable pnpm pnpm install - name: Upload to weixin run: | node scripts/release.cjs ================================================ FILE: .gitignore ================================================ # Logs *.log npm-debug.log* yarn-debug.log* yarn-error.log* # Runtime data pids *.pid *.seed *.pid.lock # Directory for instrumented libs generated by jscoverage/JSCover lib-cov # Coverage directory used by tools like istanbul coverage # nyc test coverage .nyc_output # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) .grunt # Bower dependency directory (https://bower.io/) bower_components # node-waf configuration .lock-wscript # Compiled binary addons (https://nodejs.org/api/addons.html) build/Release # Dependency directories node_modules/ jspm_packages/ # Typescript v1 declaration files typings/ # Optional npm cache directory .npm # Optional eslint cache .eslintcache # Optional REPL history .node_repl_history # Output of 'npm pack' *.tgz # Yarn Integrity file .yarn-integrity # dotenv environment variables file .env # 微信开发工具项目配置 # /project.config.json # project.config.json # MarkEditor 配置文件 .Archive .sync/* .me_configs.data .z_sync_configs.data sync_changes.log sync_hook.script **/.Archive .md_configs.data # Mac file .DS_Store **/miniprogram_npm project.private.config.json private.key private.*.key backup ================================================ FILE: .prettierignore ================================================ coverage ================================================ FILE: .prettierrc.json ================================================ { "$schema": "http://json.schemastore.org/prettierrc", "semi": true, "singleQuote": true } ================================================ FILE: .vscode/settings.json ================================================ { "files.exclude": { "**/*.js": { "when": "$(basename).ts" } } } ================================================ FILE: .yarnrc ================================================ registry "https://registry.npmmirror.com" ================================================ FILE: @types/douban/accounts.d.ts ================================================ declare namespace Douban { interface AccountInfo { name: string; weixin_binded: boolean; phone: string; avatar: Record<'medium'|'median'|'large'|'raw'|'small'|'icon', string>; id: string; uid: string; } } ================================================ FILE: @types/douban/frodo.d.ts ================================================ declare namespace Douban { interface SubjectCollection { subject_type: DouBan.SubjectType; updated_at: string; total: number; header_fg_image: string; header_bg_image: string; title: string; icon_text: string; id: string; description: string; done_count: number; } /** 榜单合集 */ interface SubjectCollectionItemsResult { count: number; subject_collection: SubjectCollection; subject_collection_items: DouBan.MovieItem[]; total: number; start: number; } /** 影院热映 */ interface SubjectCollectionShowingItemsResult { count: number subject_collection_items: Array<{ rating: { max: number value: number } cover: { url: string } year: string id: string title: string type: 'movie' info: string url: string release_date: string original_title: string uri: string }> total: number start: number } /** 即将上映 */ interface SubjectCollectionSoonItemsResult { subject_collection_items: Array<{ rating: { max: number value: number } cover: { url: string } year: string id: string title: string type: 'movie'|'tv' most_recent_release_date: string info: string url: string release_date: string original_title: string uri: string }> } interface UserInterestsResult { count: number; start: number; total: number; interests: Array<{ rating: DouBan.Rating; tags: string[]; create_time: string; status: DouBan.InterestStatus; id: string; is_private: boolean; subject: DouBan.Movie; }> } } ================================================ FILE: @types/douban/index.d.ts ================================================ declare namespace Douban { // eslint-disable-next-line @typescript-eslint/no-explicit-any interface APIResponseLegacy { status: 'success'|'failed'; message: string; description: string; payload: T; } interface LoginSuccessResult { access_token: string; douban_user_id: string; account_info: Douban.AccountInfo; douban_user_name: string; expires_in: number; refresh_token: string; } } ================================================ FILE: @types/douban.d.ts ================================================ declare namespace DouBan { /** 搜索结果 */ interface SearchResult { count: number; items: Array; total: number; start: number; } /** 搜索结果单项 */ interface SearchItem { type_name: string; layout: string; target: unknown; /** * - "tv": 电视剧 * - "movie": 电影 * - "doulist_cards": 豆列 * - "music": 音乐 * - "game": 游戏 * - "book": 书籍 */ target_type: string; } /** 搜索结果-影视单项 */ interface SearchMovieItem extends SearchItem { layout: 'subject'; target: Movie; target_type: 'tv' | 'movie'; } /** 搜索结果-豆列单项 */ interface SearchDoulistItem extends SearchItem { layout: 'doulist_cards'; target: { doulists: Array }; target_type: 'doulist_cards'; } /** 搜索结果-书籍单项 */ interface SearchBookItem extends SearchItem { layout: 'subject'; target: Book; target_type: 'book'; } interface Subject { rating: Rating; controversy_reason: string; title: string; abstract: string; uri: string; cover_url: string; card_subtitle: string; id: string; null_rating_reason: string; } type SubjectType = 'movie'|'tv'|'book'|'music'; interface Movie extends Subject { has_linewatch: boolean; year: string; } interface Book extends Subject { has_ebook: boolean; } /** 豆列 */ interface Doulists { cover_url: string; id: string; image_label: string; title: string; } interface Rating { count: number; max: number; star_count: number; value: number; } interface Actor { abstract: string; author: string | null; avatar: { large: string; normal: string }; cover_url: string; id: string; name: string; roles: string[]; sharing_url: string; title: string; type: 'celebrity'; uri: string; url: string; } interface Trailer { sharing_url: string; video_url: string; title: string; uri: string; cover_url: string; term_num: number; n_comments: number; create_time: string; subject_title: string; file_size: number; runtime: string; type: string; id: string; desc: string; } /** 影视列表单元 */ interface MovieItem { rating: Rating; cover: { url: string; width: number; height: number; }; year: string; card_subtitle: string; id: string; title: string; type: SubjectType; has_linewatch: boolean; most_recent_release_date: string; info: string; url: string; release_date: string; original_title: string; uri: string; } interface MovieDetail { id: string; actors: Actor[]; aka: string[]; body_bg_color: string; card_subtitle: string; comment_count: number; directors: Actor[]; has_linewatch: boolean; interest?: Interest; linewatches: Array<{ free: boolean; source: { literal: string; name: string; pic: string; }; source_uri: string; url: string; }>; title: string; trailers: Trailer[]; type: SubjectType; } interface User { loc: Loc; kind: string; followed: boolean; name: string; avatar_side_icon: string; url: string; gender: string; reg_time: string; uri: string; remark: string; in_blacklist: boolean; uid: string; type: string; id: string; avatar: string; } interface Loc { uid: string; id: string; name: string; } interface Interest { comment: string; rating?: Rating; sharing_url: string; tags: string[]; is_private: boolean; is_voted: boolean; uri: string; platforms: string[]; vote_count: number; create_time: string; status: InterestStatus; user: User; recommend_reason: string; user_done_desc: string; id: string; wechat_timeline_share: 'screenshot'; subject: Movie; } interface InterestResult { count: number; start: number; total: number; wechat_timeline_share: 'screenshot'; interests: Interest[] } type InterestStatus = 'mark'|'doing'|'done'; interface Image { large: ImageMeta; raw: string|null; small: ImageMeta; normal: ImageMeta; is_animated: boolean; } interface ImageMeta { url: string; width: number; size: number; height: number; } interface Photo { id: string; image: Image; } interface PhotosResult { count: number; photos: Photo[]; total: number; start: number; } interface Trailer { reaction_type: number; sharing_url: string; video_url: string; title: string; create_time: string; uri: string; cover_url: string; term_num: number; n_comments: number; reactions_count: number; is_collected: boolean; subject_title: string; collections_count: number; file_size: number; reshares_count: number; runtime: string; type: string; id: string; desc: string; } interface TrailersResult { trailers: Trailer[]; } interface ListParams { start?: number; count?: number; } interface ListResult { count: number; start: number; total: number; } } ================================================ FILE: @types/index.d.ts ================================================ import 'miniprogram-api-typings' declare namespace IMarkr { } interface RequestController extends WechatMiniprogram.RequestTask { task: WechatMiniprogram.RequestTask; } interface RequestOption extends Omit, 'success' | 'fail' | 'complete'> { baseURL?: string; controller?: RequestController; notAuthorization?: boolean } interface RequestSuccessResult extends WechatMiniprogram.RequestSuccessCallbackResult { ok: boolean; } ================================================ FILE: @types/miniprogram.d.ts ================================================ // declare namespace WechatMiniprogram { interface FormEvent extends CustomEvent { detail: { value: Detail formId: string } } namespace Component { interface A { a: string } } } ================================================ FILE: @types/mobx-miniprogram-bindings.d.ts ================================================ declare module 'mobx-miniprogram-bindings' { export const storeBindingsBehavior: WechatMiniprogram.Behavior.BehaviorIdentifier } ================================================ FILE: @types/wxCloud.d.ts ================================================ interface CloudFunctionBaseEvent { userInfo: { appId: string; openId: string }; } type CloudFunctionEvent = {}> = CloudFunctionBaseEvent & T; interface CloudFunctionContext { callbackWaitsForEmptyEventLoop: boolean; memory_limit_in_mb: number; time_limit_in_ms: number; request_id: string; environment: string; environ: string; function_version: string; function_name: string; namespace: string; tencentcloud_region: string; tencentcloud_appid: string; tencentcloud_uin: string; } interface CloudFunction = {}, Result = void> { (event: CloudFunctionEvent, context: CloudFunctionContext): Result; } interface CallCloudOptions { /** 是否显示加载提示框 */ loading?: boolean; /** 是否显示错误提示框 */ showError?: boolean; } interface CallCloud { (name: 'login', data: Record, options: CallCloudOptions): Promise; (name: 'doouban', data: Record, options: CallCloudOptions): Promise; } ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Code of conduct ### 使用2个空格进行缩进 ```javascript function hello (name) { console.log('Hi, ' + name); } ``` ### 云存储集合名采用 `_` 连接多个单词 **Bad:** ```javascript db.createCollection('cardLikeRecord') db.createCollection('card-like-record') db.createCollection('cardlikerecord') ``` **Good:** ```javascript db.createCollection('card_like_record') ``` ### 云存储属性名 - 多个单词使用下划线 `_` 连接 - 索引使用小程序提供的 `_id` - 用户的 openID 统一使用 `openid` - 记录统一增加 `create_at` 和 `update_at` 两个属性 **Bad:** ```json { "id": 12, "openID": "useropenid" } ``` **Good:** ```json { "_id": "randomstring", "openid": "useropenid", "create_at": "db.serverDate()", "update_at": "db.seerverDate()" } ``` ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: You must give any other recipients of the Work or Derivative Works a copy of this License; and You must cause any modified files to carry prominent notices stating that You changed the files; and You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright 2017 Honye Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # Mark [![Honye](https://img.shields.io/badge/Honye-红叶-red.svg)](https://honye.github.io/) [![license](https://img.shields.io/github/license/hongye567/weapp-mark.svg)](https://github.com/Honye/weapp-mark/blob/master/LICENSE)

纯属娱乐学习项目,偶尔记录下开发中遇到的问题和想法,不定期更新,如果你有什么建议也请告诉我([issues](https://github.com/Honye/weapp-mark/issues))。项目中自己有封装一些组件,可在项目结构查看。 ~~影视数据全部由[豆瓣](https://developers.douban.com/) API 提供。~~ 目前豆瓣搜索接口已经没有免费的可以使用了,本人提供的接口部署在 Vercel,未备案不可添加到微信后台,项目同时提供了 mock 数据可使用。小程序个人开发功能限制太多,无法完全上线。如若喜欢可以克隆项目自己运行看看。 ## 💥 扫码体验 小程序码 ## 🔱 分支 Branches 1. [main](https://github.com/Honye/weapp-mark/tree/main) - 采用[微信小程序云开发](https://developers.weixin.qq.com/miniprogram/dev/wxcloud/basis/getting-started.html),无需后台也能开发一款完整的小程序。 2. [master](https://github.com/Honye/weapp-mark/tree/master) - 后台服务由 [LeanCloud 云服务](https://leancloud.cn/)支撑。 3. [cdn-ui](https://github.com/Honye/weapp-mark/tree/cdn-ui) - 没有后台服务支撑,全部采用 HTTP 请求的个人博客的静态 JSON 文件。 [在线思维导图](https://www.processon.com/view/5a5c45d7e4b0abe85d562bda) **注意事项:** 使用自定义组件 [Component](https://mp.weixin.qq.com/debug/wxadoc/dev/framework/custom-component/),小程序基础版本库要在 1.6.3 以上; 使用 [wxParse](https://github.com/icindy/wxParse),小程序基础版本库要在 1.6.6 及以上。 小程序[简易双向绑定](https://developers.weixin.qq.com/miniprogram/dev/framework/view/two-way-bindings.html),小程序基础版本库 2.9.3 及以上 ## 🎨 功能 Features - 云函数实现微信登录 - 云函数定时任务实现每日卡片 - 云函数聚合查询实现卡片收藏 - Grid 多列表格布局 - Grid 布局实现瀑布流 - 云函数爬取 GitHub Trending - 关于页背景音频播放 - 分别使用 template 和 Component 实现公用组件 - CSS3 属性动画 ## 🐢 规范 Code of conduct 时间久了自己都忘记了以前给自己定的规范是啥,导致代码很不统一,给自己备份个项目规范🥱 [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md) ## 🛠 运行 Run 克隆本项目,使用微信 Web 开发工具打开项目根目录 #### 安装依赖 ```bash yarn # or npm install ``` 菜单栏 **工具 --> 构建 npm** #### 云开发环境 开通云开发环境后将项目 `/cloudfunctions/` 同步至云(参考 [云开发环境初始化](./docs/云开发环境初始化.md)),修改 `app.js` ```javascript wx.cloud.init({ traceUser: true, env: 'dev-oucwt' // 此处替换为你自己的云环境 ID }); ``` *注意:真机预览开发环境时需打开调试* #### Mock API 参考官方文档 [API Mock/规则导入导出](https://developers.weixin.qq.com/miniprogram/dev/devtools/api-mock.html),导入 [mock/mock.config.json](./mock/mock.config.json)。**开发工具提供的 Mock 能力暂不支持手机预览** **注意:** 如果你没有 AppID 可能看不到数据,手机无法预览。开发工具需要关闭安全域名的校验,**工具栏 --> 详情 --> 项目设置 --> 勾选不校验安全域名...以及 HTTPS 证书**。 ## 🪶 笔记 Notes [Vercel 托管 Next 实现 GitHub Trending API](https://github.com/Honye/notes/blob/vuepress/React/vercel-github-trending.md) [小程序使用 IconFont](https://github.com/Honye/notes/blob/vuepress/WeChat/miniprogram-iconfont.md) [云开发关联表(集合)案例](https://github.com/Hongye567/weapp-mark/wiki/小程序关联表学习) [写项目时的想法](https://github.com/Hongye567/weapp-mark/wiki/thought) [边写边记](https://github.com/Hongye567/weapp-mark/wiki/小程序笔记) [小程序使用外部字体](https://github.com/Honye/notes/blob/vuepress/WeChat/use-other-font.md) [小程序自定义评分组件 - tempalte 实现(精度 0.1)](https://github.com/Hongye567/weapp-mark/wiki/小程序自定义评分组件-template(精度0.1)) [小程序自定义评分组件 - Component 实现(精度0.1)](https://github.com/Hongye567/weapp-mark/wiki/小程序自定义评分组件-Component(精度0.1)) ## 📐 结构 Structure ``` ├── apis ├── assets ├── components 组件化 Component │ ├── cover-page 可下拉关闭的半屏组件 │ ├── pre-image 图片预加载 │ ├── rating 评分 │ └── tabs ├── cloudfunctions ├── pages │ └── common 模板 template │ ├── actionsheet 操作菜单 │ ├── cell 列表单元 │ ├── dropmenu 下拉菜单 │ ├── loading 加载/加载更多 │ ├── rating 评分 │ ├── share 底部分享菜单 │ ├── wxParse 富文本、HTML 和 MD 解析,小程序基础版本库 1.6.6 及以上 │ └── component.js wux 针对 template 的组件化,写得挺好,借鉴一下 ├── style │ ├── weui.wxss │ ├── animate.wxss CSS 动画 │ └── font-awesome.min.wxss Font Awesome 字体图标 ├── utils │ └── wxCloud.js 云函数二次封装 ├── app.js ├── app.json └── app.wxss ``` ## 痛点 - 小程序不支持全局组件,需每个页面都引入组件。如自定义全局提示框 ## 🔗 参考 Reference 1. 微信官方UI样式 [weui-wxss](https://github.com/Tencent/weui-wxss/) 2. 富文本、HTML 和 Markdown 解析 [wxParse](https://github.com/icindy/wxParse) 3. 针对 template 的自定义组件 [wux](https://github.com/skyvow/wux) 4. [LeanCloud 云服务](https://leancloud.cn/)提供后台支撑 5. [云服务开发环境(官方)](https://cloud.tencent.com/document/product/619/11447) 6. [小程序解决方案(官方)](https://cloud.tencent.com/solution/la) ================================================ FILE: cloudbaserc.json ================================================ { "version": "2.0", "region": "sh", "envId": "release-5g2g137xcedfade7", "functionRoot": "./cloudfunctions", "functions": [ { "name": "app", "runtime": "Nodejs16.13" }, { "name": "douban", "runtime": "Nodejs16.13", "triggers": [ { "name": "CronDouban", "type": "timer", "config": "0 10 0 * * * *" } ] }, { "name": "favCard", "runtime": "Nodejs16.13" }, { "name": "fetch", "runtime": "Nodejs16.13" }, { "name": "getCard", "runtime": "Nodejs16.13" }, { "name": "getCards", "runtime": "Nodejs16.13" }, { "name": "getCategories", "runtime": "Nodejs16.13" }, { "name": "getFavCards", "runtime": "Nodejs16.13" }, { "name": "initdb", "runtime": "Nodejs16.13" }, { "name": "login", "runtime": "Nodejs16.13" }, { "name": "nowPlaying", "runtime": "Nodejs16.13" }, { "name": "showingSoon", "runtime": "Nodejs16.13" }, { "name": "site", "runtime": "Nodejs16.13", "permissions": { "openapi": [ "openapi.search.submitPages" ] } }, { "name": "subscribeMessage", "runtime": "Nodejs16.13" }, { "name": "subscription", "runtime": "Nodejs16.13", "triggers": [ { "name": "trending", "type": "timer", "config": "*/10 * * * * * *" } ], "permissions": { "openapi": [ "subscribeMessage.send" ] } }, { "name": "wallpaper", "runtime": "Nodejs16.13" }, { "name": "wxacode", "runtime": "Nodejs16.13", "permissions": { "openapi": [ "wxacode.getUnlimited", "wxacode.get" ] } } ] } ================================================ FILE: cloudfunctions/app/config.json ================================================ { "permissions": { "openapi": [ ] } } ================================================ FILE: cloudfunctions/app/index.js ================================================ const cloud = require('wx-server-sdk'); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); const db = cloud.database(); exports.main = async (event, context) => { try { const { data = [] } = await db.collection('app') .orderBy('created_at', 'desc') .limit(1) .get(); return data[0]; } catch (e) { // 集合不存在 if (e.errCode === -502005) { return { version: '0.0.0' }; } throw e; } } ================================================ FILE: cloudfunctions/app/package.json ================================================ { "name": "app", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "wx-server-sdk": "~2.3.3" } } ================================================ FILE: cloudfunctions/douban/config.json ================================================ { "permissions": { "openapi": [ ] }, "triggers": [ { "name": "CronDouban", "type": "timer", "config": "0 10 0 * * * *" } ] } ================================================ FILE: cloudfunctions/douban/index.js ================================================ // 云函数入口文件 const cloud = require('wx-server-sdk'); const fetch = require('node-fetch'); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); const { request } = require('./request.js'); const db = cloud.database(); /** * 云函数入口函数 * @param {object} event * @param {'cron'|'api.proxy'|'login'|'logout'|'fetch'} [event.action] * @param {object} [event.payload] */ exports.main = async (event, context) => { /** 默认视为定时任务 */ const { action = 'cron' } = event; switch (action) { case 'cron': /** 每日定时存储每日卡片信息 */ await storeTodayItem(); break; case 'api.proxy': // case 代理接口请求,伪装请求 return apiProxy(event.payload); case 'login': return login(event.payload); case 'logout': return logout(); case 'fetch': { const { url, ...payload } = event.payload; return fetch(url, payload).then((resp) => resp.json()); } default: } const wxContext = cloud.getWXContext() return { event, openid: wxContext.OPENID, appid: wxContext.APPID, unionid: wxContext.UNIONID, } } /** * 将今日豆瓣日历卡片存入集合 * * - 用于定时任务 */ const storeTodayItem = async () => { // 云函数默认时区为 UTC+0 const res = await request({ headers: { 'User-Agent': 'api-client/0.1.3 com.douban.frodo/8.0.0' }, path: '/calendar/today', data: { date: new Intl.DateTimeFormat('zh-CN').format().replace(/\//g, '-'), alt: 'json', _sig: 'tuOyn+2uZDBFGAFBLklc2GkuQk4=', _ts: 1610703479, apiKey: '0ab215a8b1977939201640fa14c66bab' } }); return db.collection('cards').add({ data: { createTime: db.serverDate(), image: res.comment.poster, likeCount: 0, movieId: res.subject.id, quote: res.comment.content, shareCount: 0, source: `《${res.subject.title}》` } }); } /** * 存储登录用户信息及 token * @param {object} params * @param {string} params.uid * @param {string} params.user_name * @param {string} params.access_token * @param {string} params.refresh_token * @param {number} params.expires_in token 有效时间,秒为单位 */ const login = async (params) => { const wxContext = cloud.getWXContext(); const userCollection = db.collection('users'); const { data: users = [] } = await userCollection .where({ openid: wxContext.OPENID }) .limit(1) .get(); const user = users[0]; if (user) { const { expires_in: expiresIn, ...douban } = params; const updateData = { douban: { ...douban, expires_at: db.serverDate({ offset: (expiresIn - 60) * 1000 }) }, update_at: db.serverDate() }; const { _id, ...profile } = user; return await userCollection.doc(_id) .set({ data: { ...profile, ...updateData }, }); } throw new Error(`user openid=${wxContext.OPENID} not found`); } const logout = async () => { const wxContext = cloud.getWXContext(); const usersCollection = db.collection('users'); const user = await usersCollection .where({ openid: wxContext.OPENID }) .limit(1) .get() .then(({ data }) => data[0]); if (user) { return usersCollection.doc(user._id) .update({ data: { douban: null }, update_at: db.serverDate(), }); } throw new Error(`user openid=${wxContext.OPENID} not found`); }; /** * * @param {WechatMiniprogram.RequestOption} params */ const apiProxy = (params) => { const headers = Object.assign({}, { Referer: 'https://servicewechat.com/wx2f9b06c1de1ccfca/81/page-frame.html', 'User-Agent': 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 MicroMessenger/8.0.2(0x18000236) NetType/WIFI Language/en' }, params.header ); return request({ url: params.url, method: params.method, data: params.data, headers }); } /** * @typedef {'movie_showing'|'movie_soon'} RankType */ /** * 各个排行榜 * @param {object} params * @param {RankType} params.type * @param {object} params.params */ const getRankList = async (params) => { const collectionName = params.type; /** @type {Record} */ const names = { movie_showing: '正在热映', movie_soon: '即将上映' }; /** * @type {{ * _id: string; * update_time: Date; * }} */ let stored; try { const ranks = await db.collection(collectionName) .where({ key: collectionName }) .limit(1) .get(); stored = ranks[0]; } catch (e) { if (e.errCode === -502005) { await db.createCollection(collectionName); } } if (stored && stored.update_time.getTime() + 2 * 60 * 60 * 1000 > Date.now()) { // 每两小时一更新 return stored.data; } const res = await apiProxy(params.params); if (stored) { // case 更新 db.collection(collectionName) .doc(stored._id) .update({ data: { update_time: db.serverDate(), data: res } }); } else { // case 存储 db.collection(collectionName) .add({ data: { key: collectionName, title: names[collectionName], create_time: db.serverDate(), update_time: db.serverDate(), data: res } }); } return res; } ================================================ FILE: cloudfunctions/douban/package.json ================================================ { "name": "douban", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "node-fetch": "^2.6.1", "wx-server-sdk": "~2.3.3" } } ================================================ FILE: cloudfunctions/douban/request.d.ts ================================================ export const request: (params: { headers?: Record url?: string path?: string method?: 'GET' data?: Record }) => Promise ================================================ FILE: cloudfunctions/douban/request.js ================================================ const cloud = require('wx-server-sdk'); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); const fetch = require('node-fetch'); const qs = require('querystring'); const API_BASE = 'https://frodo.douban.com/api/v2'; const API_KEY = '054022eaeae0b00e0fc068c0c0a2102a'; /** * * @param {object} params * @param {object} [params.headers] * @param {string} [params.url] * @param {string} [params.path] * @param {'GET'} [params.method] * @param {object} [params.data] */ const request = async (params = {}) => { params.url = params.url || `${API_BASE}${params.path}`; delete params.path; params.data = { apikey: API_KEY, ...(params.data || {}) }; if ((params.method || 'GET') === 'GET' && params.data) { params.url += '?' + qs.stringify(params.data); delete params.data; } return fetch( params.url, { headers: params.headers, method: params.method || 'GET', body: params.data } ) .then((resp) => resp.json()); } module.exports = { request } ================================================ FILE: cloudfunctions/favArticle/index.js ================================================ // 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init( // { // env: 'dv-963c46' // } ) const db = cloud.database() /** 与用户关联 */ const relateUser = (event, context) => { const { userInfo: { openId }, id: articleID } = event return new Promise((resolve, reject) => { db.collection('userRelations').where({ _openid: openId, }).get().then(({ data }) => { if (data.length) { let { favoriteArticles } = data[0] const has = favoriteArticles && favoriteArticles.includes(articleID) if (typeof favoriteArticles === 'undefined') { favoriteArticles = [articleID] } else if (has) { favoriteArticles = favoriteArticles.filter(item => item !== articleID) } else { favoriteArticles = Array.from(new Set([articleID, ...favoriteArticles])) } db.collection('userRelations').doc(data[0]._id).update({ data: { favoriteArticles } }).then(res => { resolve({ message: has ? '取消喜欢' : '喜欢成功' }) }) } else { db.collection('userRelations').add({ data: { _openid: openId, favoriteArticles: [articleID], } }).then(res => { resolve({ message: '喜欢成功' }) }) } }).catch(err => { reject(err) }) }) } /** 与文章关联 */ const relateArticle = (event, context) => { const { userInfo: { openId }, id: articleID } = event return new Promise((resolve, reject) => { db.collection('articleRelations').where({ id: articleID, }).get().then(({ data }) => { if (data.length) { let { users } = data[0] const has = users && users.includes(openId) if (typeof users === 'undefined') { users = [openId] } else if (has) { users = users.filter(item => item !== openId) } else { users = Array.from(new Set([openId, ...users])) } db.collection('articleRelations').doc(data[0]._id).update({ data: { users } }).then( res => { resolve({ message: has ? '取消喜欢' : '喜欢成功' }) }).catch( err => { reject(err) }) } else { db.collection('articleRelations').add({ data: { id: articleID, users: [openId], } }).then( res => { resolve({ message: '喜欢成功' }) }).catch( err => { reject(err) }) } }).catch( err => { reject(err) }) }) } // 云函数入口函数 exports.main = async (event, context) => { return Promise.all([ relateUser(event, context), relateArticle(event, context) ]).then( values => values[0] || values) } ================================================ FILE: cloudfunctions/favArticle/package.json ================================================ { "name": "favArticle", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "wx-server-sdk": "latest" } } ================================================ FILE: cloudfunctions/favCard/index.js ================================================ // 云函数入口文件 const cloud = require('wx-server-sdk'); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); const db = cloud.database(); // 云函数入口函数 exports.main = async (event, context) => { const { id: cardID } = event; const { OPENID } = cloud.getWXContext(); const collectionName = 'card_like'; let record; try { const records = await db.collection(collectionName) .where({ openid: OPENID, card_id: cardID }) .limit(1) .get(); record = records.data[0]; } catch (e) { if (e.errCode === -502005) { /** 集合不存在,新创建集合 */ await db.createCollection(collectionName); } } if (record) { return await db.collection(collectionName) .doc(record._id) .update({ data: { update_at: db.serverDate(), state: Number(!record.state) } }); } /** 不存在集合时肯定是做喜欢操作 */ return await db.collection(collectionName) .add({ data: { openid: OPENID, create_at: db.serverDate(), update_at: db.serverDate(), card_id: cardID, state: 1 } }); } ================================================ FILE: cloudfunctions/favCard/package.json ================================================ { "name": "favCard", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "wx-server-sdk": "latest" } } ================================================ FILE: cloudfunctions/fetch/config.json ================================================ { "permissions": { "openapi": [ ] } } ================================================ FILE: cloudfunctions/fetch/index.js ================================================ const cloud = require('wx-server-sdk') const fetch = require('node-fetch') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) exports.main = async (event, context) => { const { url, ...options } = event return fetch(url, options) .then((resp) => { const data = resp.json() if (resp.ok) { return data } return Promise.reject(data) }) } ================================================ FILE: cloudfunctions/fetch/package.json ================================================ { "name": "fetch", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "node-fetch": "~2.6.12", "wx-server-sdk": "~2.6.3" } } ================================================ FILE: cloudfunctions/getArticleDetails/index.js ================================================ // 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init( // { // env: 'dv-963c46' // } ) const db = cloud.database() /** 获取是否喜欢 */ const getFavStatus = (event, context) => { const { userInfo: { openId }, id: articleID } = event return db.collection('userRelations').where({ _openid: openId, }).get().then(({ data }) => { let result = false if (data.length) { result = data[0].favoriteArticles && data[0].favoriteArticles.includes(articleID) } return { data: result, message: 'success', } }).catch( err => { return Promise.reject({ data: false, message: err.errMsg || JSON.stringify(err), }) }) } // 云函数入口函数 exports.main = async (event, context) => { return getFavStatus(event, context) } ================================================ FILE: cloudfunctions/getArticleDetails/package.json ================================================ { "name": "getArticleDetails", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "wx-server-sdk": "latest" } } ================================================ FILE: cloudfunctions/getCard/config.json ================================================ { "permissions": { "openapi": [ ] } } ================================================ FILE: cloudfunctions/getCard/index.js ================================================ const cloud = require('wx-server-sdk'); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }); const db = cloud.database(); const _ = db.command; const $ = _.aggregate; exports.main = async (event, context) => { const wxContext = cloud.getWXContext(); const openid = wxContext.OPENID; const _id = event._id; const promiseCard = db.collection('cards').doc(_id).get().then(({ data }) => data); const promiseLiked = db.collection('card_like') .where({ card_id: _id, state: 1 }) .count() .then(({ total }) => total); const promiseState = db.collection('card_like') .where({ card_id: _id, openid }) .limit(1) .get() .then(({ data }) => data[0] && data[0].state); const [card, liked, state] = await Promise.all([promiseCard, promiseLiked, promiseState]); return { ...card, like_count: liked, like_state: state, }; } ================================================ FILE: cloudfunctions/getCard/package.json ================================================ { "name": "getCard", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "wx-server-sdk": "~2.6.1" } } ================================================ FILE: cloudfunctions/getCards/index.js ================================================ // 云函数入口文件 const cloud = require('wx-server-sdk'); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); const db = cloud.database(); const _ = db.command; // 云函数入口函数 exports.main = async (event, context) => { const $ = db.command.aggregate; const { OPENID } = cloud.getWXContext(); const res = await db.collection('cards').aggregate() .sort({ createTime: -1 }) .limit(6) .lookup({ from: 'card_like', let: { card_id: '$_id' }, pipeline: $.pipeline() .match(_.expr($.and([ $.eq(['$card_id', '$$card_id']), $.eq(['$state', 1]) ]))) .done(), as: 'like_list' }) .addFields({ like_count: $.size('$like_list') }) .addFields({ like_state: $.let({ vars: { filtered: $.filter({ input: '$like_list', as: 'item', cond: $.and($.eq(['$$item.openid', OPENID]), $.eq(['$$item.state', 1])) }) }, in: $.reduce({ input: '$$filtered', initialValue: 0, in: '$$this.state' }) }) }) .end(); return res; } ================================================ FILE: cloudfunctions/getCards/package.json ================================================ { "name": "getCards", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "wx-server-sdk": "latest" } } ================================================ FILE: cloudfunctions/getCategories/index.js ================================================ // 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() const _ = db.command const getCategories = async (event, context) => { let ret = [] let message = 'success' const pidData = await db.collection('categories') .aggregate() .group({ _id: '$pid', }) .sort({ _id: 1 }) .end() const pids = pidData.list || [] for (let i = 0, len = pids.length; i < len; ++i) { const pCateData = await db.collection('categories') .where({ id: pids[i]._id, }) .get() if (pCateData.data && pCateData.data.length) { const pCate = pCateData.data[0] const children = await db.collection('categories') .where({ pid: pCate.id, id: _.neq(pCate.id), }) .get() .then(({ data = [] }) => data) pCate.children = children ret.push(pCate) } else { console.log(`未找到 id 为 \`${pids[i]._id}\` 的分类`) } } return { data: ret, message, } } // 云函数入口函数 exports.main = async (event, context) => { return getCategories(event, context) } ================================================ FILE: cloudfunctions/getCategories/package.json ================================================ { "name": "getCategories", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "wx-server-sdk": "latest" } } ================================================ FILE: cloudfunctions/getFavArticles/index.js ================================================ // 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init( // { // env: 'dv-963c46' // } ) const db = cloud.database() const _= db.command /** 获取喜欢的影单 */ const getFavArticles = (event, context) => { const { userInfo: { openId } } = event return db.collection('userRelations').where({ _openid: openId, }).get().then(({ data }) => { if (data.length) { return db.collection('articles').where({ id: _.in(data[0].favoriteArticles ? data[0].favoriteArticles.map(item => Number(item)) : []), }).field({ id: true, image: true, likeCount: true, title: true, }).get().then(({ data }) => { return { data, message: 'success', } }).catch( err => { console.error('*** 错误 ***', err) return Promise.reject({ data: [], message: err.errMsg || JSON.stringify(err) }) }) } else { return { data: [], message: 'success', } } }).catch( err => { console.error('*** 错误 ***', err) return Promise.reject({ data: [], message: err.errMsg || JSON.stringify(err) }) }) } // 云函数入口函数 exports.main = async (event, context) => { return getFavArticles(event, context) } ================================================ FILE: cloudfunctions/getFavArticles/package.json ================================================ { "name": "getFavArticles", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "wx-server-sdk": "latest" } } ================================================ FILE: cloudfunctions/getFavCards/index.js ================================================ // 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV, }) const db = cloud.database() const _ = db.command const $ = _.aggregate /** 获取用户喜欢的卡片 */ const getFavCards = async (event, context) => { // const { userInfo: { openId } } = event const { OPENID: openId } = cloud.getWXContext(); const records = await db.collection('card_like') .aggregate() .match({ openid: openId, state: 1, }) .lookup({ from: 'cards', localField: 'card_id', foreignField: '_id', as: 'card_detail', }) // 过滤掉卡片已不存在(删除)的数据 .match({ card_detail: _.elemMatch({ _id: _.exists(true), }), }) // 只返回卡片信息 .replaceRoot({ newRoot: $.arrayElemAt(['$card_detail', 0]), }) .end(); return records; } // 云函数入口函数 exports.main = async (event, context) => { return getFavCards(event, context) } ================================================ FILE: cloudfunctions/getFavCards/package.json ================================================ { "name": "getFavCards", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "wx-server-sdk": "latest" } } ================================================ FILE: cloudfunctions/github/config.json ================================================ { "permissions": { "openapi": [ ] } } ================================================ FILE: cloudfunctions/github/fetch.js ================================================ /** * [GitHub REST API](https://docs.github.com/en/rest) 封装 * * 使用原生 fetch api * */ const cloud = require('wx-server-sdk'); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); const fetch = require('node-fetch'); const qs = require('querystring'); const db = cloud.database(); const baseURL = 'https://api.github.com'; /** * * @param {object} params * @param {string} [params.url] * @param {string} params.path * @param {'GET'|'POST'} [params.method] * @param {Record} params.data */ const request = async (params = {}) => { let token; try { token = await getToken(); } catch (e) { console.warn(e); } params.url = params.url || `${baseURL}${params.path}`; delete params.path; if ((params.method || 'GET') === 'GET') { params.url += '?' + qs.stringify(params.data); delete params.data; } return fetch(params.url, { headers: { Accept: 'application/vnd.github.v3+json', Authorization: `token ${token}`, }, method: params.method || 'GET', body: params.data }); }; /** * * @returns {Promise} */ const getToken = async () => { const users = db.collection('users'); const context = cloud.getWXContext(); const openId = context.OPENID; const { data: [user] } = await users.where({ openid: openId }) .limit(1) .get(); if (!user) { return; } return user.githubToken; }; module.exports = { request }; ================================================ FILE: cloudfunctions/github/index.js ================================================ /** * GitHub 相关云函数入口 */ /** * @typedef {object} ResponseType 云函数调用成功响应 * @property {number} code 0:成功;-1:失败 * @property {any} data 请求结果 * @property {string} message 请求信息 */ const cloud = require('wx-server-sdk'); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); const { request } = require('./fetch'); const db = cloud.database(); /** * 云函数入口函数 * @param {object} event * @param {string} event.action * @param {object} event.data */ exports.main = async (event, context) => { const { action } = event; switch (action) { case 'setAccessToken': return setAccessToken(event.data); case 'getStarredList': return getStarredList(event.data); case 'removeAccessToken': return removeAccessToken(event.data); case 'homePage': return getHomePage(event.data); default: } } const queryUser = async () => { const users = db.collection('users'); const context = cloud.getWXContext(); const openId = context.OPENID; const { data: [user] } = await users.where({ openid: openId }) .limit(1) .get(); return user; }; /** * 设置 GitHub AccessToken * * [new token](https://github.com/settings/tokens/new) * * @param {object} params * @param {string} params.token */ const setAccessToken = async (params) => { const users = db.collection('users'); const context = cloud.getWXContext(); const openId = context.OPENID; const updateTime = new Date(); const { data: [user] } = await users.where({ openid: openId }) .limit(1) .get(); if (!user) { /** @type {ResponseType} */ const ret = { code: -1, message: 'user not found', }; return ret; } await users.doc(user._id) .update({ data: { update_at: updateTime, githubToken: params.token } }); /** @type {ResponseType} */ const ret = { code: 0, message: 'set github token success' }; return ret; }; const removeAccessToken = async () => { const user = await queryUser(); /** @type {ResponseType} */ const ret = { code: 0, message: 'remove github token success' }; if (!user) { ret.code = -1; ret.message = 'user not found'; return ret; } const users = db.collection('users'); await users.doc(user._id) .update({ data: { update_at: db.serverDate(), githubToken: null } }); return ret; }; /** * 获取用户 Star repositories * * [List repositories starred by the authenticated user](https://docs.github.com/en/rest/reference/activity#list-repositories-starred-by-the-authenticated-user) * * @param {object} params * @param {'created'|'updated'} params.sort * @param {'asc'|'desc'} params.direction * @param {number} params.per_page max 100 * @param {number} params.page */ const getStarredList = async (params) => { const resp = await request({ path: '/user/starred', data: params }) .then(resp => resp.json()); return { code: 0, data: resp }; }; /** * GitHub user home page info * * @param {object} params * @param {string} params.user */ const getHomePage = async (params) => { return request({ url: `https://www.imarkr.com/api/github/${params.user}` }).then((resp) => resp.json()); }; ================================================ FILE: cloudfunctions/github/package.json ================================================ { "name": "github", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "node-fetch": "^2.6.1", "wx-server-sdk": "~2.3.2" } } ================================================ FILE: cloudfunctions/initdb/index.js ================================================ const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) const db = cloud.database() /** * 初始化表 banners */ const initBanners = async () => { console.log('*** start initBanners ***') const collection = 'banners' await db.createCollection(collection) .catch((e) => { // 集合已存在 }) const banners = [{ image: 'http://static.zhidao.manmankan.com/kimages/201609/26_1474884213331602.jpg', type: 'image', }, { image: 'http://www.stdaily.com/zhuanti01/2017sc/2017-09/29/581248/images/188f4c63b10547bebf39131bb25c1162.png', type: '', }, { image: 'https://pbs.twimg.com/media/Cr9fC-dUMAABqEC.jpg', }, ] let docLength = 0 await db.collection(collection) .get() .then(({ data }) => { docLength = data.length }) for (let i = 0, length = banners.length; i < length; ++i) { await db.collection(collection).add({ data: { id: docLength + i + 1, createTime: db.serverDate(), ...banners[i] } }).then(res => { console.log('*** add banner successed ***', res) }).catch(err => { console.error('*** add banner failed ***', err) }) } console.log('*** initBanners finished ***') } /** * 初始化表 articles */ const initArticles = async () => { console.log('*** start init articles ***') const articles = [ { id: 1125, image: "http://7xqnv7.com2.z0.glb.qiniucdn.com/2017-12-21_5a3b66c4e3c92.jpg", title: "那些温情电影都告诉了我们什么?!", likeCount: 104 }, { id: 1118, image: "http://7xqnv7.com2.z0.glb.qiniucdn.com/2017-12-17_5a362f082c25f.jpg", title: "我想知道流星能飞多久", likeCount: 99 }, { id: 1119, image: "http://7xqnv7.com2.z0.glb.qiniucdn.com/2017-12-18_5a376fc20e33e.jpg", title: "《月升王国》谁说大人的世界没有童话?", likeCount: 33 }, { id: 1102, image: "http://7xqnv7.com2.z0.glb.qiniucdn.com/2017-12-03_5a236db32a980.jpg", title: "每一个童年都值得被爱:《西葫芦的生活》", likeCount: 69 }, { id: 1074, image: "http://7xqnv7.com2.z0.glb.qiniucdn.com/2017-11-04_59fd63d65798d.jpg", title: "《蝴蝶梦》:她低到了尘埃里,终将摆脱前任的阴影", likeCount: 25 }, { id: 1062, image: "http://7xqnv7.com2.z0.glb.qiniucdn.com/2017-10-27_59f2d62abb85d.jpg", title: "《生吃》才不是史上最恐怖的影片,但成长确实是一个思之极恐的过程", likeCount: 107 }, { id: 1059, image: "http://7xqnv7.com2.z0.glb.qiniucdn.com/2017-10-27_59f2af5ae403b.jpg", title: "谁的人生不迷茫?今敏惊悚悬疑电影《未麻的部屋》观影感悟", likeCount: 55 }, ] for (let i = 0, length = articles.length; i < length; ++i) { await db.collection('articles').add({ data: { createTime: db.serverDate(), ...articles[i] } }).then(res => { console.log('*** add article successed ***', res) }).catch(err => { console.error('*** add article failed ***', err) }) } console.log('*** init articles finished ***') } /** * 初始化表 cards */ const initCards = async () => { console.log('*** start init cards ***') const collection = 'cards' await db.createCollection(collection) .catch(() => { // 集合已存在 }) const cards = [ { id: 11, "image": "https://img3.doubanio.com/view/photo/raw/public/p579724050.jpg", quote: "人心其实很脆弱,所以我们要经常哄哄它,经常把手放在心脏旁,对自己说:平安无事,平安无事,平安无事……", source: "《三傻大闹宝莱坞》", likeCount: 84, shareCount: 12, movieId: 3793023 }, { id: 10, image: "https://img1.doubanio.com/view/photo/raw/public/p2245932509.jpg", quote: "当恋爱了,就算最枯燥的工作也能忍受。", source: "《海街日记》", likeCount: 51, shareCount: 10, movieId: 25895901 }, { id: 9, image: "https://img1.doubanio.com/view/photo/l/public/p456514379.jpg", quote: "为了你在乎的东西值得冒险。", source: "《南极大冒险》", likeCount: 52, shareCount: 11, movieId: 1477448 }, { id: 8, image: "https://img3.doubanio.com/view/photo/l/public/p1716666870.jpg", quote: "我一直以为最糟糕的情况是你离开我,其实最令我难过的,是你不快乐。", source: "《精灵旅社》", likeCount: 121, shareCount: 19, movieId: 3269068 }, { id: 7, image: "https://img3.doubanio.com/view/photo/l/public/p1106861692.jpg", quote: "贬低他人,并不会令自己变得多伟大。", source: "《贱女孩》", likeCount: 64, shareCount: 11, movieId: 1309084 }, { id: 6, image: "https://img3.doubanio.com/view/photo/l/public/p2374369280.jpg", quote: "尽管已提前知晓人生,以及它的走向,我无所畏惧,并且会珍惜每一分钟。", source: "《降临》", likeCount: 63, shareCount: 10, movieId: 21324900 }, { id: 5, image: "https://img3.doubanio.com/view/photo/l/public/p1694419516.jpg", quote: "不要让别人告诉你,你不能做什么。只要有梦想,就要去追求。那些做不到的人总要告诉你,你也不行。想要什么就得去努力,去追求。", source: "《当幸福来敲门》", likeCount: 74, shareCount: 15, movieId: 1849031 }, { id: 4, image: "https://img3.doubanio.com/view/photo/l/public/p1140053562.jpg", quote: "真正的失败者不是那些没有赢的人,而是那些害怕失败而不敢尝试的人。", source: "《阳光小美女》", likeCount: 98, shareCount: 18, movieId: 1777612 }, { id: 3, image: "https://img3.doubanio.com/view/photo/l/public/p800791080.jpg", quote: "探戈走错了可以重来,人生则不可...", source: "《闻香识女人》", likeCount: 81, shareCount: 11, movieId: 1298624 } ] for (let i = 0, length = cards.length; i < length; ++i) { await db.collection(collection).add({ data: { createTime: db.serverDate(), ...cards[i] } }).then(res => { console.log('*** add card successed ***', res) }).catch(err => { console.error('*** add card failed ***', err) }) } console.log('*** init cards finished ***') } exports.main = async(event, context) => { console.log('*** 开始初始化 ***') await initBanners() // await initArticles() await initCards() console.log('*** 初始化结束 ***') } ================================================ FILE: cloudfunctions/initdb/package.json ================================================ { "name": "douban", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "wx-server-sdk": "latest" } } ================================================ FILE: cloudfunctions/login/config.json ================================================ { "permissions": { "openapi": [ ] } } ================================================ FILE: cloudfunctions/login/index.js ================================================ // 云函数入口文件 const cloud = require('wx-server-sdk'); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); const db = cloud.database(); const login = async (event, context) => { const wxUserInfo = event.wxUserInfo && event.wxUserInfo.data; if (typeof wxUserInfo === 'object') { // 😞用户表里字段和微信返回的字段大小写不一致,避免存多字段,替换字段 wxUserInfo.openid = wxUserInfo.openId || wxUserInfo.openid; delete wxUserInfo.openId; wxUserInfo.unionid = wxUserInfo.unionId || wxUserInfo.unionid; delete wxUserInfo.unionId; } try { await db.createCollection('users'); } catch (err) { // "users" collection already exist // do nothing } const usersCollection = db.collection('users'); const wxContext = cloud.getWXContext(); const { data: users } = await usersCollection .where({ openid: wxContext.OPENID }) .limit(1) .get(); if (users && users.length) { // case 老用户登录,更新用户信息 const user = users[0]; const serverDate = db.serverDate(); const updateData = { latest_login: serverDate, update_at: serverDate }; if (typeof wxUserInfo === 'object') { // 每次登录都更新最新信息 updateData.rawData = wxUserInfo; for (const key in wxUserInfo) { if ( Object.prototype.hasOwnProperty.call(wxUserInfo, key) && !isInvalid(wxUserInfo[key]) ) { updateData[key] = wxUserInfo[key]; } } } await usersCollection.doc(user._id) .update({ data: updateData }); const updatedUser = { ...user, ...updateData }; if (updatedUser.douban) { const { expires_at: expiresAt } = updatedUser.douban; if (!expiresAt || new Date(expiresAt).getTime() < Date.now()) { // 豆瓣登录已失效,清除 access_token updatedUser.douban.access_token = ''; } } return { data: updatedUser, message: 'cloud.login:ok' }; } // case 新用户登录 const serverDate = db.serverDate(); const user = { openid: wxContext.OPENID, appid: wxContext.APPID, unionid: wxContext.UNIONID, create_at: serverDate, update_at: serverDate, latest_login: serverDate }; if (typeof wxUserInfo === 'object') { // 每次登录都更新最新信息 user.rawData = wxUserInfo; for (const key in wxUserInfo) { if ( Object.prototype.hasOwnProperty.call(wxUserInfo, key) && !isInvalid(wxUserInfo[key]) ) { user[key] = wxUserInfo[key]; } } } const { _id } = await usersCollection.add({ data: user }); return { data: { _id, ...user }, message: 'cloud.login:ok' }; }; const isInvalid = (data) => { return data === undefined || data === null || data === '' || (typeof data === 'number' && isNaN(data)); } // 云函数入口函数 exports.main = async (event, context) => { return await login(event, context); } ================================================ FILE: cloudfunctions/login/package.json ================================================ { "name": "login", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "wx-server-sdk": "~2.2.0" } } ================================================ FILE: cloudfunctions/nowPlaying/config.json ================================================ { "permissions": { "openapi": [ ] } } ================================================ FILE: cloudfunctions/nowPlaying/fetch.js ================================================ const cheerio = require('cheerio'); const fetch = require('node-fetch'); const fetchTrendingList = async () => { const data = await fetch('https://github.com/trending'); const $ = cheerio.load(await data.text()); const allTitles = $('.Box .Box-row') .get() .map(repo => { const $repo = $(repo); const title = $repo.find('.h3').text().trim(); const [owner, name] = title.split('/').map(v => v.trim()); return `${owner}/${name}`; }); console.log(allTitles); }; const fetchNowPlaying = async () => { const data = await fetch('https://movie.douban.com/cinema/nowplaying/wuhan/'); const $ = cheerio.load(await data.text()); const movies = $('ul.lists .list-item') .get() .map(movie => { const $movie = $(movie); const id = $movie.attr('id'); const title = $movie.attr('data-title'); const realTime = $movie.attr('data-release'); const duration = $movie.attr('data-duration'); const rating = $movie.attr('data-score'); const img = $movie.find('.poster img') .attr('src'); return { id, title, realTime, durations: [duration], rating, img }; }); console.log('movies===', movies); return movies; }; module.exports = { fetchTrendingList, fetchNowPlaying }; ================================================ FILE: cloudfunctions/nowPlaying/index.js ================================================ // 云函数入口文件 const cloud = require('wx-server-sdk'); const { fetchNowPlaying } = require('./fetch'); cloud.init() // 云函数入口函数 exports.main = async (event, context) => { const list = await fetchNowPlaying(); return list; } ================================================ FILE: cloudfunctions/nowPlaying/package.json ================================================ { "name": "nowPlaying", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "cheerio": "^1.0.0-rc.3", "node-fetch": "^2.6.1", "wx-server-sdk": "~2.2.0" } } ================================================ FILE: cloudfunctions/showingSoon/config.json ================================================ { "permissions": { "openapi": [ ] } } ================================================ FILE: cloudfunctions/showingSoon/index.js ================================================ // 云函数入口文件 const cloud = require('wx-server-sdk'); const cheerio = require('cheerio'); const fetch = require('node-fetch'); cloud.init(); const fetchShowingSoon = async () => { const data = await fetch('https://movie.douban.com/cinema/later/shanghai/'); const $ = cheerio.load(await data.text()); const movies = $('#showing-soon .item') .get() .map(movie => { const $movie = $(movie); const title = $movie.find('.intro h3').text().trim(); const realTime = $movie.find('.intro li.dt:nth-child(1)').text().trim(); const genres = $movie.find('.intro li.dt:nth-child(2)').text().trim(); const img = $movie.find('a.thumb img') .attr('src'); return { title, realTime, genres, img }; }); console.log('movies===', movies); return movies; }; // 云函数入口函数 exports.main = async (event, context) => { const list = await fetchShowingSoon(); return list; } ================================================ FILE: cloudfunctions/showingSoon/package.json ================================================ { "name": "showingSoon", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "cheerio": "^1.0.0-rc.3", "node-fetch": "^2.6.0", "wx-server-sdk": "~2.2.0" } } ================================================ FILE: cloudfunctions/site/config.json ================================================ { "permissions": { "openapi": [ "openapi.search.submitPages" ] } } ================================================ FILE: cloudfunctions/site/index.js ================================================ // 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) /** * * @param {object} event * @param {'submitPages'} event.action * @param {object} [event.payload = {}] * @param {Array<{ path: string; query: string; }>} [event.payload.pages] */ exports.main = async (event, context) => { const { action, payload = {} } = event; switch (action) { case 'submitPages': return await cloud.openapi.search.submitPages(payload); default: } } ================================================ FILE: cloudfunctions/site/package.json ================================================ { "name": "site", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "wx-server-sdk": "~2.4.0" } } ================================================ FILE: cloudfunctions/subscribeMessage/index.js ================================================ // 云函数入口文件 const cloud = require('wx-server-sdk') cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }) // 云函数入口函数 exports.main = async () => { const wxContext = cloud.getWXContext() console.log('openid==>', wxContext.OPENID) try { const result = await cloud.openapi.subscribeMessage.send({ touser: wxContext.OPENID, templateId: 'sJz8Heo9GSqMwhnJFlpEHbm-rmIhUlhOkEOoSvY6BwE', data: { thing1: { value: '《Dr.STONE》', }, time2: { value: '2019-12-16 10:00:00' }, name3: { value: 'Honye' }, } }) return result } catch (err) { console.error(err) return err } } ================================================ FILE: cloudfunctions/subscribeMessage/package.json ================================================ { "name": "subscribeMessage", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "wx-server-sdk": "latest" } } ================================================ FILE: cloudfunctions/subscription/config.json ================================================ { "triggers": [ { "name": "trending", "type": "timer", "config": "*/10 * * * * * *" } ], "permissions": { "openapi": [ "subscribeMessage.send" ] } } ================================================ FILE: cloudfunctions/subscription/index.js ================================================ // 云函数入口文件 const cloud = require('wx-server-sdk'); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); const db = cloud.database(); const send = async (openid, data) => { return await cloud.openapi.subscribeMessage.send({ touser: openid, templateId: 'sJz8Heo9GSqMwhnJFlpEHbm-rmIhUlhOkEOoSvY6BwE', data: { thing1: { value: '《Dr.STONE》', }, time2: { value: '2019-12-16 10:00:00' }, name3: { value: 'Honye' }, } }) }; // 云函数入口函数 exports.main = async (event, context) => { const usersCollection = db.collection('users'); const { data: users } = await usersCollection .limit(1) .get(); for (const user of users) { await send(user.openid); } return { data: null, message: 'cloud.subscription:ok' } } ================================================ FILE: cloudfunctions/subscription/package.json ================================================ { "name": "subscription", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "wx-server-sdk": "~2.2.0" } } ================================================ FILE: cloudfunctions/trending/config.json ================================================ { "permissions": { "openapi": [ ] } } ================================================ FILE: cloudfunctions/trending/index.js ================================================ // 云函数入口文件 const cloud = require('wx-server-sdk'); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); const fetch = require('node-fetch'); const cheerio = require('cheerio'); const qs = require('querystring'); /** * 已迁移至 Vercel */ // eslint-disable-next-line @typescript-eslint/no-unused-vars const usedInVercel = async () => { const data = await fetch('https://github.com/trending'); const $ = cheerio.load(await data.text()); const list = $('.Box .Box-row') .get() .map(repo => { const $repo = $(repo); const title = $repo.find('.h3').text().trim(); const [owner, name] = title.split('/').map(v => v.trim()); const description = $(($repo.children())[2]).text().trim(); const language = $repo.find('[itemprop="programmingLanguage"]').text().trim(); const starCount = $repo.find('[aria-label="star"].octicon.octicon-star').parent().text().trim(); return { owner: { login: owner, avatar_url: `https://github.com/${owner}.png` }, name, description, language, stargazers_count: starCount }; }); return list; }; const fetchTrendingList = async (params) => { let url = 'https://www.imarkr.com/api/trending'; if (params) { const query = qs.stringify(params); if (query) { url += `?${query}`; } } const data = await fetch(url).then((resp) => resp.json()); return data; } /** * 云函数入口函数 * * @param {object} event * @param {string} [event.language] * @param {'daily'|'weekly'|'monthly'} [event.since] * @param {string} [event.spoken_language_code] */ exports.main = async (event, context) => { return fetchTrendingList(event); } ================================================ FILE: cloudfunctions/trending/package.json ================================================ { "name": "trending", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "cheerio": "^1.0.0-rc.5", "node-fetch": "^2.6.1", "wx-server-sdk": "~2.2.0" } } ================================================ FILE: cloudfunctions/wallpaper/index.js ================================================ const fetch = require('node-fetch') const fetchCategories = async () => { const res = await fetch('http://wallpaper.apc.360.cn/index.php?c=WallPaper&a=getAllCategories') .then((resp) => { const data = resp.json() if (resp.ok) return data return Promise.reject(data) }) await Promise.allSettled( Object.values(res.data).map((c) => fetchListByCategory({ cid: c.id, count: 1 }) .then((w) => c.cover = w.data[0]?.url) ) ) return res } const fetchListByCategory = async (data) => { const base = 'http://wallpaper.apc.360.cn/index.php?c=WallPaper&a=getAppsByCategory' const params = new URLSearchParams(data) const url = `${base}&${params}` return fetch(url) .then((resp) => { const data = resp.json() if (resp.ok) return data return Promise.reject(data) }) } exports.main = async (event, context) => { const { action, payload } = event return { fetchCategories, fetchListByCategory }[action]?.(payload) } ================================================ FILE: cloudfunctions/wallpaper/package.json ================================================ { "name": "wallpaper", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "node-fetch": "~2.6.12", "wx-server-sdk": "~2.6.3" } } ================================================ FILE: cloudfunctions/wxacode/config.json ================================================ { "permissions": { "openapi": [ "wxacode.getUnlimited", "wxacode.get" ] } } ================================================ FILE: cloudfunctions/wxacode/index.js ================================================ // 云函数入口文件 const cloud = require('wx-server-sdk'); cloud.init({ env: cloud.DYNAMIC_CURRENT_ENV }); // 云函数入口函数 exports.main = async (event, context) => { const { contentType, buffer } = await cloud.openapi.wxacode.get({ path: 'pages/tabs/discovery/discovery' }); const base64 = buffer.toString('base64'); return `data:${contentType};base64,${base64}`; } ================================================ FILE: cloudfunctions/wxacode/package.json ================================================ { "name": "wxacode", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "author": "", "license": "ISC", "dependencies": { "wx-server-sdk": "~2.2.0" } } ================================================ FILE: docs/云开发环境初始化.md ================================================ # 云开发环境初始化 ## 创建环境 登录腾讯云[云开发控制台](https://console.cloud.tencent.com/tcb/platform/env) 在环境管理创建一个环境,如果已有环境可跳过这一步 可以在网页端的控制台创建环境也可在微信开发者工具的云开发面板创建环境 ![CloudBase Env](./screenshots/cloudbase_env.png) ## 部署云函数 可以使用微信开发者工具部署,也可以使用 CloudBase CLI 部署 微信开发者工具只能逐个部署,但稳 CloudBase CLI 可以批量部署,但可能会出现异常 ### 使用微信开发者工具部署 右键项目根目录下 `/cloudfunctions/` 目录选择当前环境 ![](./screenshots/function_env.png) 选择 `/cloudfunctions/` 目录下的一级目录创建并部署云函数 ![](./screenshots/function_deploy.png) ### 使用 CloudBase CLI 部署 将项目根目录下的 `/cloudbaserc.json` 中的 `envId` 和 `region` 改为自己的环境 id 和地域 | 地域 | 全称 | 缩写 | | :--- | :--- | :--- | | 上海 | `ap-shanghai` | `sh` | | 广州 | `ap-guangzhou` | `gz` | | 北京 | `ap-beijing` | `bj` | ```json { "envId": "release-5g2g137xcedfade7", "region": "sh" } ``` 全局安装 CloudBase CLI。可选择你喜欢的任意包管理工具,这里使用 pnpm 作为示例 ```shell pnpm add -g @cloudbase/cli tcb -v ``` 执行如下命令通过网页授权登录 ```shell tcb login ``` 部署云函数 ```shell tcb fn deploy ``` 云函数部署成功后微信开发者工具会显示为绿色 ![云函数部署成功示意图](./screenshots/function_deployed.png) ## 初始化集合数据 终端进入 `/cloudfunctions/initdb/` 安装依赖 ```shell pnpm i ``` 在微信开发者工具右键目录 `/cloudfunctions/initdb/` 开启云函数本地调试,手动调用一次云函数 `initdb` 即可 ================================================ FILE: jest.config.js ================================================ /* * For a detailed explanation regarding each configuration property and type check, visit: * https://jestjs.io/docs/configuration */ export default { // All imported modules in your tests should be mocked automatically // automock: false, // Stop running tests after `n` failures // bail: 0, // The directory where Jest should store its cached dependency information // cacheDirectory: "/private/var/folders/kr/f89s8xwj7vj75cn5rhpj3hq00000gn/T/jest_dx", // Automatically clear mock calls, instances, contexts and results before every test clearMocks: true, // Indicates whether the coverage information should be collected while executing the test collectCoverage: true, // An array of glob patterns indicating a set of files for which coverage information should be collected // collectCoverageFrom: undefined, // The directory where Jest should output its coverage files coverageDirectory: "coverage", // An array of regexp pattern strings used to skip coverage collection // coveragePathIgnorePatterns: [ // "/node_modules/" // ], // Indicates which provider should be used to instrument code for coverage // coverageProvider: "babel", // A list of reporter names that Jest uses when writing coverage reports // coverageReporters: [ // "json", // "text", // "lcov", // "clover" // ], // An object that configures minimum threshold enforcement for coverage results // coverageThreshold: undefined, // A path to a custom dependency extractor // dependencyExtractor: undefined, // Make calling deprecated APIs throw helpful error messages // errorOnDeprecated: false, // The default configuration for fake timers // fakeTimers: { // "enableGlobally": false // }, // Force coverage collection from ignored files using an array of glob patterns // forceCoverageMatch: [], // A path to a module which exports an async function that is triggered once before all test suites // globalSetup: undefined, // A path to a module which exports an async function that is triggered once after all test suites // globalTeardown: undefined, // A set of global variables that need to be available in all test environments // globals: {}, // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. // maxWorkers: "50%", // An array of directory names to be searched recursively up from the requiring module's location // moduleDirectories: [ // "node_modules" // ], // An array of file extensions your modules use // moduleFileExtensions: [ // "js", // "mjs", // "cjs", // "jsx", // "ts", // "tsx", // "json", // "node" // ], // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module // moduleNameMapper: {}, // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader // modulePathIgnorePatterns: [], // Activates notifications for test results // notify: false, // An enum that specifies notification mode. Requires { notify: true } // notifyMode: "failure-change", // A preset that is used as a base for Jest's configuration // preset: undefined, // Run tests from one or more projects // projects: undefined, // Use this configuration option to add custom reporters to Jest // reporters: undefined, // Automatically reset mock state before every test // resetMocks: false, // Reset the module registry before running each individual test // resetModules: false, // A path to a custom resolver // resolver: undefined, // Automatically restore mock state and implementation before every test // restoreMocks: false, // The root directory that Jest should scan for tests and modules within // rootDir: undefined, // A list of paths to directories that Jest should use to search for files in // roots: [ // "" // ], // Allows you to use a custom runner instead of Jest's default test runner // runner: "jest-runner", // The paths to modules that run some code to configure or set up the testing environment before each test // setupFiles: [], // A list of paths to modules that run some code to configure or set up the testing framework before each test // setupFilesAfterEnv: [], // The number of seconds after which a test is considered as slow and reported as such in the results. // slowTestThreshold: 5, // A list of paths to snapshot serializer modules Jest should use for snapshot testing // snapshotSerializers: [], // The test environment that will be used for testing // testEnvironment: "jest-environment-node", // Options that will be passed to the testEnvironment // testEnvironmentOptions: {}, // Adds a location field to test results // testLocationInResults: false, // The glob patterns Jest uses to detect test files testMatch: [ // "**/__tests__/**/*.[jt]s?(x)", // "**/?(*.)+(spec|test).[tj]s?(x)" "**/*.spec.js" ], // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped // testPathIgnorePatterns: [ // "/node_modules/" // ], // The regexp pattern or array of patterns that Jest uses to detect test files // testRegex: [], // This option allows the use of a custom results processor // testResultsProcessor: undefined, // This option allows use of a custom test runner // testRunner: "jest-circus/runner", // A map from regular expressions to paths to transformers transform: {}, // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation // transformIgnorePatterns: [ // "/node_modules/", // "\\.pnp\\.[^\\/]+$" // ], // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them // unmockedModulePathPatterns: undefined, // Indicates whether each individual test should be reported during the run // verbose: undefined, // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode // watchPathIgnorePatterns: [], // Whether to use watchman for file crawling // watchman: true, }; ================================================ FILE: miniprogram/apis/cloud/index.js ================================================ const db = wx.cloud.database(); export const getBanners = () => { return db.collection('banners') .orderBy('id', 'desc') .limit(4) .get() .then(({ data }) => data); }; ================================================ FILE: miniprogram/apis/douban/accounts.js ================================================ import { request as baseRequest } from '../../utils/request'; import wxCloud from '../../utils/wxCloud'; /** 豆瓣小程序 AppID */ const AppID = 'wx2f9b06c1de1ccfca'; /** * @template T * @param {RequestOption>} config */ const request = async (config) => { const { header, ...rest } = config; const _config = { baseURL: 'https://accounts.douban.com/j/wxa', header: { 'Content-Type': 'application/x-www-form-urlencoded; charset=UTF-8', ...header, }, ...rest, }; /** @type {RequestSuccessResult>} */ const resp = await baseRequest(_config); if (resp.ok && resp.data.status === 'success') { return resp.data; } return Promise.reject(resp.data); }; /** * 登录 * @param {object} params * @param {string} params.name 用户名 * @param {string} params.password 密码 * @param {string} [params.appid] * @param {string} [params.phone] * @param {string} [params.captcha_id] * @param {string} [params.captcha_solution] * @param {string} [params.ticket] * @param {string} [params.randstr] * @returns */ export const login = async (params) => { /** @type {Douban.APIResponseLegacy} */ const res = await request({ url: '/login/basic', method: 'POST', data: { appid: AppID, ...params }, }); wxCloud('douban', { action: 'login', payload: { access_token: res.payload.access_token, refresh_token: res.payload.refresh_token, expires_in: res.payload.expires_in, ...res.payload.account_info, } }); return res; } /** * 获取手机验证码 * @param {object} params * @param {string} [params.area_code] 区域号,如:"+86" * @param {number} params.number 手机号 */ export const getCaptcha = async (params) => { params = { appid: AppID, area_code: '+86', ...params, } const res = await request({ url: '/login/request_phone_code', method: 'POST', data: params, }); return res; } /** * 验证验证码 * @param {object} params * @param {string} params.number * @param {string} params.code * @returns */ export const verifyCaptcha = async (params) => { params = { area_code: '+86', appid: AppID, ...params, }; /** @type {Douban.APIResponseLegacy} */ const res = await request({ url: '/login/verify_phone_code', method: 'POST', data: params, }); return res.payload; }; ================================================ FILE: miniprogram/apis/douban/request.js ================================================ import wxCloud from '../../utils/wxCloud'; export class RequestController { /** @type {WechatMiniprogram.RequestTask} */ task; constructor() { return new Proxy(this, { get(target, prop) { if (Object.prototype.hasOwnProperty.call(target, prop)) { return Reflect.get(target, prop); } else { return target.task[prop]; } } }); } } /** * @template T * @param {RequestOption} config * @returns {Promise} * @example * * ``` * const controller = new RequestController(); * request({ * ...config, * controller * }); * // 中断请求任务 * controller.abort(); * ``` */ export const request = ({ baseURL, controller, ...config }) => { return new Promise((resolve, reject) => { const requestTask = wx.request( /** @type {WechatMiniprogram.RequestOption} */ ({ ...config, url: `${baseURL}${config.url}`, success: (res) => { res.ok = res.statusCode >= 200 && res.statusCode < 300; resolve(res); }, fail: (err) => reject(err), }) ); if (controller) { controller.task = requestTask; } }); }; /** * @template T * @param {*} options * @returns {Promise} */ export const cloudFetch = (options) => { return wxCloud('douban', { action: 'fetch', payload: options, }); }; ================================================ FILE: miniprogram/apis/douban.js ================================================ /** * @file 豆瓣 API */ import { request as baseRequest } from '../utils/request'; import { store } from '../store/index'; /** * @type {{ params: RequestOption, resolve: (data: any) => void, reject: (err: any) => void }[]} */ const queue = []; export let isLoginIng = false; /** * @param {boolean} value */ export const setLoginIng = (value) => { isLoginIng = value; }; const BASE_URL = 'https://mmovie.imarkr.com/douban/api'; /** * @template T * @param {RequestOption} params * @returns {Promise} */ const request = (params) => { const accessToken = store.douban.accessToken; const { header, notAuthorization, ...rest } = { baseURL: BASE_URL, ...params, }; return new Promise((resolve, reject) => { baseRequest({ header: { ...(!notAuthorization && accessToken && { Authorization: `Bearer ${accessToken}` }), ...header, }, ...rest, }) .then((resp) => { if (resp.ok) { resolve(resp.data); } else if (resp.statusCode === 400 && [103, 106].includes(resp.data.code)) { queue.push({ params, resolve, reject }); if (!isLoginIng) { isLoginIng = true; wx.navigateTo({ url: '/packages/douban/pages/login-phone/login-phone' }); } } else { reject(resp.data); } }); }); }; /** 登录成功后重放登录失效接口 */ export const replayRequest = () => { while (queue.length) { const { params, resolve, reject } = queue.shift(); request(params).then(resolve, reject); } }; /** * 搜索 * @param {object} params * @param {string} params.q 关键字 * @param {number} params.start 起始位置 * @param {number} params.count 查询数量 * @returns {Promise} */ export const search = (params) => { return request({ url: '/search/weixin', data: params }); }; /** * 影视详情 * @param {object} params * @param {string} params.id * @param {'movie'|'tv'} [params.type] * @returns {Promise} */ export const getDetail = (params) => { const { id, type = 'movie' } = params; return request({ url: `/${type}/${id}` }); }; /** * 影人列表 * @param {object} params * @param {string} params.id * @param {'movie'|'tv'} [params.type] * @returns {Promise<{ * actors: DouBan.Actor[]; * directors: DouBan.Actor[]; * total: number; * }>} */ export const getCelebrities = (params) => { const { id, type = 'movie' } = params; return request({ url: `/${type}/${id}/celebrities` }); }; /** * 短评列表 * @param {object} params * @param {string} params.id * @param {number} params.start * @param {number} params.count * @param {'done'} [params.status] * @param {'movie'|'tv'} [params.type = 'movie'] * @returns {Promise} */ export const getInterests = (params) => { const { id, type = 'movie', ...data } = params; return request({ url: `/${type}/${id}/interests`, data }); }; /** * 剧照 * @param {object} params * @param {string} params.id * @param {number} params.start * @param {number} params.count * @param {'movie'|'tv'} [params.type = 'movie'] * @returns {Promise} */ export const getPhotos = (params) => { const { id, type = 'movie', ...data } = params; return request({ url: `/${type}/${id}/photos`, data }); }; /** * 预告片列表 * @param {object} params * @param {string} params.id * @returns {Promise} */ export const getTrailers = (params) => { return request({ url: `/movie/${params.id}/trailers` }); }; /** * 豆瓣热门 * @param {object} params * @param {number} [params.start] * @param {number} [params.count] * @returns {Promise<{ * count: number; * subject_collection_items: DouBan.MovieItem[]; * total: number; * start: number; * }>} */ export const getHotMovies = (params) => { return request({ url: '/subject_collection/movie_hot_gaia/items', data: params, notAuthorization: true }); } /** * 榜单合集 * @param {object} params * @param {string} params.type * @param {number} [params.start] * @param {number} [params.count] * @returns {Promise} */ export const getCollectionList = (params) => { const { type, ...data } = params; return request({ url: `/subject_collection/${type}/items`, data }); } /** * 影院热映 * @param {object} params * @param {number} [params.start] * @param {number} [params.count] * @returns {Promise} */ export const getShowingMovies = (params) => { return request({ url: '/subject_collection/movie_showing/items', data: params }); } /** * 即将上映 * @param {object} params * @param {number} [params.start] * @param {number} [params.count] * @returns {Promise} */ export const getSoonMovies = (params) => { return request({ url: '/subject_collection/movie_soon/items', data: params }); } /** * 获取用户的书影音 * @param {string} userID * @param {object} params * @param {'movie'} params.type * @param {'doing'} params.status * @param {number} [params.start] * @param {number} [params.count] * @returns {Promise} */ export const getUserInterests = (userID, params) => { return request({ url: `/user/${userID}/interests`, data: params }); } /** * 标记影视为想看 * @param {object} params * @param {string} params.movieID * @param {DouBan.SubjectType} [params.type = 'movie'] * @param {number} [params.rating] * @param {0|1} [params.sync_douban] * @returns {Promise} */ export const markMovie = (params) => { const { movieID, type = 'movie', ...rest } = params; const data = Object.assign({}, { raing: 0, sync_douban: 0 }, rest); return request({ url: `/${type}/${movieID}/mark`, data }); } /** * 删除影视标记 * @param {object} params * @param {string} params.movieID * @param {DouBan.SubjectType} [params.type = 'movie'] * @returns {Promise<{ * comment: string; * status: DouBan.InterestStatus; * id: string; * }>} */ export const unmarkMovie = (params) => { return request({ url: `/${params.type || 'movie'}/${params.movieID}/unmark`, }); } /** * 标记影视为已看 * @param {object} params * @param {string} params.movieID * @param {'movie'|'tv'} [params.type = 'movie'] * @param {number} [params.rating] * @param {string} [params.comment] * @param {0|1} [params.sync_douban] * @returns {Promise} */ export const doneMovie = (params) => { const { movieID, type = 'movie', ...rest } = params; const data = Object.assign({}, { rating: 0, sync_douban: 0 }, rest ); return request({ url: `/${type}/${movieID}/done`, data }); } /** * 标记影视为已看 * @param {object} params * @param {string} params.movieID * @param {DouBan.SubjectType} [params.type = 'movie'] * @param {number} [params.rating] * @param {string} [params.comment] * @param {0|1} [params.sync_douban] * @returns {Promise} */ export const doingMovie = (params) => { const { movieID, type = 'movie', ...rest } = params; const data = Object.assign({}, { rating: 0, sync_douban: 0 }, rest ); return request({ url: `/${type}/${movieID}/doing`, data }); } ================================================ FILE: miniprogram/apis/github.js ================================================ /** * [GitHub API v3](https://docs.github.com/en/rest) */ import { store } from '../store/index'; /** * * @param {WechatMiniprogram.RequestOption} options * @returns {Promise} result */ const request = (options) => { const { header, method, url, data, ...restOpt } = options; const token = store.user.info?.githubToken; if (Object.prototype.toString.call(data) === '[object Object]') { Object.keys(data).forEach((key) => { const v = data[key]; if (!v && v !== 0 && v !== false) { delete data[key]; } }); } return new Promise((resolve, reject) => { wx.request({ header: { Accept: 'application/vnd.github.v3+json', Authorization: token ? `token ${token}` : undefined, ...header }, method: method || 'GET', url: `https://api.github.com${url}`, success: ({ statusCode, data }) => { if (statusCode >= 200 && statusCode < 300) { resolve(data); } else { reject({ ...data, message: data.message || '服务器开小差了' }); } }, fail: (err) => { reject(err); }, data, ...restOpt }) }); }; /** * [Get a repository](https://docs.github.com/en/rest/reference/repos#get-a-repository) * * @param {Object} params * @param {string} params.owner * @param {string} params.repo */ export const getRepoInfo = (params) => { return request({ method: 'GET', url: `/repos/${params.owner}/${params.repo}` }); }; /** * [Get repository README](https://docs.github.com/en/rest/reference/repos#get-a-repository-readme) * * @param {object} params * @param {'raw'|'html'} [params.media] [Custom media types for repository contents](https://docs.github.com/en/rest/reference/repos#custom-media-types-for-repository-contents) * @param {string} params.owner * @param {string} params.repo * @param {string} [params.ref] */ export const getRepoReadme = (params) => { const { owner, repo, ...query } = params; return request({ header: { Accept: params.media ? `application/vnd.github.v3.${params.media}` : `application/vnd.github.v3+json` }, method: 'GET', url: `/repos/${owner}/${repo}/readme`, data: query }); }; /** * * @param {object} params * @param {string} params.username * @param {number} params.per_page * @param {number} params.page */ export const getEvents = (params) => { const { username, ...reset } = params; return request({ method: 'GET', url: `/users/${username}/received_events`, data: reset }); }; /** * [List notifications for the authenticated user](https://docs.github.com/en/rest/reference/activity#list-notifications-for-the-authenticated-user) * * @param {object} params * @param {boolean} [params.all] * @param {boolean} [params.participating] * @param {string} [params.since] a timestamp in ISO 8601 format: YYYY-MM-DDTHH:MM:SSZ * @param {string} [params.before] * @param {number} [params.per_page] * @param {number} [params.page] */ export const getNotifications = (params) => { return request({ method: 'GET', url: '/notifications', data: params }); }; /** * https://docs.github.com/en/rest/reference/search#constructing-a-search-query * * @typedef {object} Query * @property {string} q * @property {string} sort * @property {number} per_page * @property {number} page */ /** * [Search repositories](https://docs.github.com/en/rest/reference/search#search-repositories) * * @param {Query} params * @returns {Promise<{ * total_count: number; * incomplete_results: boolean; * items: Array<{ * id: number; * name: string; * full_name: string; * }>; * owner: User; * private: boolean; * created_at: string; * updated_at: string; * pushed_at: string; * homepage: string; * stargazers_count: number; * watchers_count: number; * language: string; * forks_count: number; * default_branch: string; * archived: boolean; * disabled: boolean; * }>} */ export const searchRepositories = (params) => { return request({ header: { Accept: 'application/vnd.github.v3.text-match+json' }, method: 'GET', url: '/search/repositories', data: params }); }; /** * [Search users](https://docs.github.com/en/rest/reference/search#search-users) * * @param {Query} params */ export const searchUser = (params) => { return request({ header: { Accept: 'application/vnd.github.v3.text-match+json' }, method: 'GET', url: '/search/users', data: params }); }; /** * 获取用户 Star repositories * * [List repositories starred by the authenticated user](https://docs.github.com/en/rest/reference/activity#list-repositories-starred-by-the-authenticated-user) * * @param {object} params * @param {'created'|'updated'} params.sort * @param {'asc'|'desc'} params.direction * @param {number} params.per_page max 100 * @param {number} params.page */ export const getStarredList = (params) => { return request({ url: '/user/starred', method: 'GET', data: params }); }; /** * 获取已授权的用户信息 * * [Get the authenticated user](https://docs.github.com/en/rest/reference/users#get-the-authenticated-user) */ export const getSelfInfo = () => { return request({ url: '/user', method: 'GET' }); } /** * @typedef {{ * login: string; * id: number; * avatar_url: string; * }} User */ ================================================ FILE: miniprogram/apis/leancloud/index.js ================================================ import * as AV from '../../libs/av-live-query-core-min'; /** * @returns {Promise} */ export const getBanners = async () => { const query = new AV.Query('Banner'); const data = await query.find(); return data.map((item) => { const { attributes, ...cols } = item; return { ...cols, ... attributes }; }); }; ================================================ FILE: miniprogram/apis/server/index.js ================================================ import { request } from '../../utils/request' /** * @param {RequestOption} config */ const fetch = async (config) => { config.baseURL = 'https://sanphantom.com/go' const resp = await request(config) if (resp.ok) { return resp } return Promise.reject(resp.data) } export const apiGetBanners = () => { return fetch({ url: '/banners' }) } export const apiGetHotMovies = (data) => { return fetch({ url: '/douban/hot/items', data }) } export const apiGetShowingMovies = (data) => { return fetch({ url: '/douban/showing/items', data }) } export const apiGetSoonMovies = (data) => { return fetch({ url: '/douban/soon/items', data }) } export const apiGetDetail = (params) => { const { id, type = 'movie' } = params return fetch({ url: `/douban/${type}/${id}` }) } export const apiGetCelebrities = (params) => { const { id, type = 'movie' } = params return fetch({ url: `/douban/${type}/${id}/celebrities` }) } export const apiSearch = (data) => { return fetch({ url: '/douban/search', data }) } export const apiGetPhotos = (params) => { const { id, type = 'movie', ...data } = params return fetch({ url: `/douban/${type}/${id}/photos`, data }) } export const apiGetInterests = (params) => { const { id, type = 'movie', ...data } = params return fetch({ url: `/douban/${type}/${id}/interests`, data }) } export const apiGetUserInterests = (userID, data) => { return fetch({ url: `/douban/user/${userID}/interests`, data }) } export const apiGetCaptcha = (data) => { return fetch({ url: '/douban/login/request_phone_code', method: 'POST', data }) } export const apiVerifyCaptcha = (data) => { return fetch({ url: '/douban/login/verify_phone_code', method: 'POST', data }) } ================================================ FILE: miniprogram/apis/vercel.js ================================================ import { request } from '../utils/request' import { decrypt } from '../utils/crypro'; /** * @param {RequestOption} config */ const fetch = async (config) => { config.baseURL = 'https://mmovie.imarkr.com' const resp = await request(config) if (resp.ok) { return resp.data } return Promise.reject(resp.data) } export const apiAppInfo = async () => { const { data } = await fetch({ url: '/wx/app' }); return JSON.parse(decrypt(data)); }; /** * 验证登录验证码(登录) * @param {object} data * @param {string} data.number 手机号 * @param {string} data.code 验证码 * @param {string} data.openid * @param {string} data.unionid * @param {string} [data.captcha_id] * @param {string} [data.captcha_solution] */ export const apiVerifyCaptcha = (data) => { return fetch({ url: '/douban/verify_phone_code', method: 'POST', data }); }; export const apiSyncDouban = (data) => { return fetch({ url: '/douban/sync', method: 'POST', data }); }; /** * @param {object} data * @param {string} data.code */ export const apiWxLogin = (data) => { return fetch({ url: '/wx/login', data }) }; export const apiGetBanners = () => { return fetch({ url: '/banners' }) } export const apiGetCards = () => { return fetch({ url: '/m/cards', method: 'POST' }); }; export const apiSubmitPages = (data) => { return fetch({ url: '/wx/submitpages', method: 'POST', headers: { 'Content-Type': 'application/json' }, data }) } ================================================ FILE: miniprogram/app.js ================================================ import { store } from './store/index'; import { compareVersions, isEmpty } from './utils/util'; import { apiAppInfo, apiWxLogin } from './apis/vercel'; wx.cloud.init({ traceUser: true, env: 'dev-oucwt' }); App({ globalData: { userInfo: null, setting: {}, config: null }, onLaunch () { this.getSetting(); this.getDefaultConfig(); this.login(); }, /** 通过云函数直接登录 */ async login () { const { code } = await new Promise((resolve, reject) => { wx.login({ success: resolve, fail: reject }); }); const data = await apiWxLogin({ code }); this.globalData.userInfo = data; store['user/updateUserInfo'](data); if (data.access_token) { const { access_token, refresh_token, ...user } = data; store['douban/update']({ accessToken: access_token, refreshToken: refresh_token, user }); } if (data.douban) { const { access_token, refresh_token, ...user } = data.douban; store['douban/update']({ accessToken: access_token, refreshToken: refresh_token, user }); } }, /** * 获取用户信息 * 支持 callback 和 Promise * @param {function} cb (object:userInfo) => void */ getUserInfo (cb) { return new Promise((resolve, reject) => { if (this.globalData.userInfo) { typeof cb === 'function' && cb(this.globalData.userInfo); resolve(this.globalData.userInfo); } else { wx.login({ success: () => { wx.getUserInfo({ success: res => { this.globalData.userInfo = res.userInfo typeof cb === 'function' && cb(this.globalData.userInfo) resolve(this.globalData.userInfo) } }); } }); } }); }, /** 从服务器获取默认配置 */ async getDefaultConfig () { const appInfo = await apiAppInfo(); store['app/update']({ hasPublished: compareVersions(store.app.version, appInfo.version) <= 0, ...appInfo }); }, /** 退出登录 */ logout (callback) { this.globalData.userInfo = null; callback && callback(this.globalData); }, /** 获取本地设置 */ getSetting (callback) { const { setting } = this.globalData; if (setting && (!isEmpty(setting))) { typeof callback == "function" && callback(setting); } else { wx.getStorage({ key: 'setting', success: res => { this.globalData.setting = res.data; typeof callback == "function" && callback(res.data); } }); } } }) ================================================ FILE: miniprogram/app.json ================================================ { "pages": [ "pages/splash/splash", "pages/tabs/discovery/discovery", "pages/tabs/index/index", "pages/tabs/movies/movies", "pages/setting/setting", "pages/marked/marked", "pages/search/search", "pages/about/about", "pages/first/first", "pages/webview/index", "pages/test/test", "packages/wallpaper/pages/categories/categories" ], "window": { "backgroundTextStyle": "dark", "navigationBarBackgroundColor": "#fff", "navigationBarTitleText": "iMark", "navigationBarTextStyle": "black", "backgroundColor": "#f7f7f7" }, "tabBar": { "custom": true, "color": "#496069", "selectedColor": "#000", "backgroundColor": "#fff", "borderStyle": "black", "list": [ { "pagePath": "pages/tabs/discovery/discovery", "text": "发现", "iconPath": "assets/images/tabbar/tabbar_discovery.png", "selectedIconPath": "assets/images/tabbar/tabbar_discovery_selected.png" }, { "pagePath": "pages/tabs/movies/movies", "text": "清单", "iconPath": "assets/images/tabbar/tabbar_list.png", "selectedIconPath": "assets/images/tabbar/tabbar_list_selected.png" }, { "pagePath": "pages/tabs/index/index", "text": "我的", "iconPath": "assets/images/tabbar/tabbar_mine.png", "selectedIconPath": "assets/images/tabbar/tabbar_mine_selected.png" } ] }, "subPackages": [ { "root": "packages/article/", "pages": [ "pages/categories/categories", "pages/details/detail", "pages/movie-list-detail/movie-list-detail" ] }, { "root": "packages/movie/", "pages": [ "pages/cards/card", "pages/details/details", "pages/intheaters/in_theaters", "pages/trailers/trailers", "pages/photos/photos", "pages/mark/mark" ] }, { "root": "packages/user/", "pages": [ "pages/favMovieList/index", "pages/evaluate/evaluate", "pages/favCards/index", "pages/userinfo/userinfo", "pages/achievement/achievement" ] }, { "root": "packages/github/", "pages": [ "pages/home/home", "pages/notifications/notifications", "pages/starred/starred", "pages/trending/trending", "pages/languages/languages", "pages/repository/repository", "pages/search/search" ] }, { "root": "packages/douban/", "pages": [ "pages/login-phone/login-phone", "pages/collection/collection" ] }, { "root": "packages/example/", "pages": [ "pages/index/index", "pages/cover-page/cover-page", "pages/tab-bar/tab-bar", "pages/waterfall/waterfall", "pages/icons/icons" ] }, { "root": "packages/admin/", "pages": [ "pages/index/index", "pages/app/app", "pages/douban/douban" ] }, { "root": "packages/tools", "pages": [ "pages/index/index", "pages/encode/encode", "pages/random/random" ] } ], "preloadRule": { "pages/tabs/discovery/discovery": { "network": "all", "packages": ["packages/article/", "packages/movie/"] }, "pages/tabs/index/index": { "network": "all", "packages": ["packages/github/"] }, "packages/article/pages/categories/categories": { "network": "all", "packages": ["packages/douban/"] } }, "usingComponents": {}, "requiredBackgroundModes": ["audio"], "sitemapLocation": "sitemap.json" } ================================================ FILE: miniprogram/app.wxss ================================================ /**app.wxss**/ @import './style/weui.wxss'; @import '/style/animate.wxss'; @import './style/font-awesome.min.wxss'; @import './style/iconfont.wxss'; @import "/templates/wxParse/wxParse.wxss"; @import '/style/common.wxss'; ================================================ FILE: miniprogram/components/Tabs/Tabs.js ================================================ // Tabs Component({ options: { addGlobalClass: true, }, /** * 组件的属性列表 */ properties: { tabs: { type: Array, value: [], }, }, /** * 组件的初始数据 */ data: { currentIndex: 0, }, /** * 组件的方法列表 */ methods: { _handleTabTap(e) { const { index } = e.currentTarget.dataset const { currentIndex } = this.data if (index !== currentIndex) { this.setData({ currentIndex: index, }) this.triggerEvent('change', { value: index }) } this.triggerEvent('itemtap', { value: index }) }, } }) ================================================ FILE: miniprogram/components/Tabs/Tabs.json ================================================ { "component": true, "usingComponents": {} } ================================================ FILE: miniprogram/components/Tabs/Tabs.wxml ================================================ {{item}} ================================================ FILE: miniprogram/components/Tabs/Tabs.wxss ================================================ /* Tabs */ .tabs { /* position: fixed; */ z-index: 99; top: 0; left: 0; right: 0; display: flex; white-space: nowrap; padding: 16rpx 0; background: #fff; } .tabs .tabs-item { display: inline-block; flex: 1; text-align: center; } .tabs-item .tabs-item__content { display: inline-block; font-size: 36rpx; line-height: 1; color: #999; font-weight: 400; position: relative; z-index: 1; transition: all .3s; } .tabs-item.is-active .tabs-item__content { color: #333; font-size: 42rpx; } .tabs-item.is-active .tabs-item__content::before { content: ''; display: block; position: absolute; z-index: -1; height: 8rpx; bottom: 0; left: 0; right: 0; background: #FFE200; } ================================================ FILE: miniprogram/components/article/article.js ================================================ // components/article/article.js Component({ /** * 组件的属性列表 */ properties: { aid: { type: String, value: '' }, image: { type: String, value: '' }, title: { type: String, value: '' }, likeCount: { type: Number, value: 0 }, checked: { type: Boolean, value: false } }, /** * 组件的初始数据 */ data: { }, /** * 组件的方法列表 */ methods: { onFavChange(e){ this.triggerEvent('change', { checked: e.detail.checked, }) } } }) ================================================ FILE: miniprogram/components/article/article.json ================================================ { "component": true, "usingComponents": { "comp-btnFav": "/components/btn-fav/FavButton" } } ================================================ FILE: miniprogram/components/article/article.wxml ================================================ {{title}} {{likeCount}} ================================================ FILE: miniprogram/components/article/article.wxss ================================================ /* components/article/article.wxss */ .item-bgimg { margin: 10rpx; height: 360rpx; border-radius: 10rpx; background-repeat: no-repeat; background-size: 100% 100%; display: flex; align-items: flex-end; position: relative; overflow: hidden; } .item-bgimg .gradient { position: absolute; width: 100%; height: 100%; top: 0; z-index: 1; display: flex; align-items: flex-end; background: -webkit-gradient(linear, left 0, left 165%, color-stop(0,transparent), color-stop(1,#000000)); background: -moz-linear-gradient(top, transparent, #000000 165%); } .item-bgimg .item-title { color: #fff; padding: 15rpx; font-weight: bold; } .item-bgimg .item-count { position: absolute; display: flex; flex-direction: column; align-items: center; top: 0; right: 20rpx; padding: .5em .3em .3em; background-color: rgba(0,0,0,.5); border-radius: 0 0 10rpx 10rpx; z-index: 2; } .item-bgimg .item-count>image { width: 1.2em; height: 1.2em; } .item-bgimg .item-count>text { color: #fff; font-size: .8em; } ================================================ FILE: miniprogram/components/btn-fav/FavButton.js ================================================ // @ts-check Component({ properties: { height: { type: String, value: '2em' }, width: { type: String, value: '2em' }, checked: { type: Boolean, value: false }, iconNormal: { type: String, value: '/assets/images/icon-unlike.png' }, iconChecked: { type: String, value: '/assets/images/icon-liked.png' }, }, data: {}, methods: { _onToggle (e) { const { checked } = this.data; this.triggerEvent('change', { checked }) } } }) ================================================ FILE: miniprogram/components/btn-fav/FavButton.json ================================================ { "component": true, "usingComponents": {} } ================================================ FILE: miniprogram/components/btn-fav/FavButton.wxml ================================================ ================================================ FILE: miniprogram/components/btn-fav/FavButton.wxss ================================================ /* components/btn-fav/FavButton.wxss */ .wrapper { display: flex; width: 2em; height: 2em; } .icon-like { width: 100%; height: 100%; animation: zoomIn .5s ease-in; } @keyframes zoomIn { from { -webkit-transform: scale3d(1, 1, 1); transform: scale3d(1, 1, 1); } 5% { -webkit-transform: scale3d(1.1, 1.1, 1); transform: scale3d(1.1, 1.1, 1); } 12.5% { -webkit-transform: scale3d(1, 1, 1); transform: scale3d(1, 1, 1); } 20% { -webkit-transform: scale3d(0.9, 0.9, 1); transform: scale3d(0.9, 0.9, 1); } 25% { -webkit-transform: scale3d(1.05, 1.05, 1); transform: scale3d(1.05, 1.05, 1); } 32.5% { -webkit-transform: scale3d(1, 1, 1); transform: scale3d(1, 1, 1); } 100% { -webkit-transform: scale3d(1, 1, 1); transform: scale3d(1, 1, 1); } } ================================================ FILE: miniprogram/components/cover-page/cover-page.js ================================================ Component({ options: { addGlobalClass: true }, properties: { visible: { type: Boolean, value: false }, title: { type: String, value: '' }, distance: { type: Number, value: 60 } }, methods: { /** * @param {object} params * @param {boolean} params.visible */ setVisible ({ visible }) { this.setData({ visible }); }, show () { this.setVisible({ visible: true }); }, hide () { this.setVisible({ visible: false }); }, prevent () { // avoid warning } } }); ================================================ FILE: miniprogram/components/cover-page/cover-page.json ================================================ { "component": true, "usingComponents": {} } ================================================ FILE: miniprogram/components/cover-page/cover-page.wxml ================================================ {{title}} ================================================ FILE: miniprogram/components/cover-page/cover-page.wxs ================================================ function handleModalTouchMove (event, ownerInstance) { var state = ownerInstance.getState(); var currentY = event.changedTouches[0].pageY; if (state.startY === undefined) { // 开始滑动的地方。scroll-view 滚动时并不会触发此函数,也就不会有 startY state.startY = currentY; } var diff = currentY - state.startY; event.instance.setStyle({ transition: 'none', transform: 'translate3d(0, ' + (diff < 0 ? 0 : diff) + 'px, 0)' }); } function handleModalTouchEnd (event, ownerInstance) { var state = ownerInstance.getState(); var currentY = event.changedTouches[0].pageY; var diff = currentY - state.startY; var compModal = ownerInstance.selectComponent('#coverPage'); var dataset = event.currentTarget.dataset; if (diff > dataset.distance) { compModal.removeClass('is-show'); ownerInstance.callMethod('setVisible', { visible: false }); } else { ownerInstance.callMethod('setVisible', { visible: true }); } // 还原 startY,便于下次记录 state.startY = undefined; } function observeVisible (newValue, oldValue, ownerInstance, instance) { var compModal = ownerInstance.selectComponent('#coverPage'); if (newValue) { instance.setStyle({ transform: 'translate3d(0, 0, 0)' }); compModal.addClass('is-show'); } else { instance.setStyle({}); compModal.removeClass('is-show'); } } module.exports = { handleModalTouchMove: handleModalTouchMove, handleModalTouchEnd: handleModalTouchEnd, observeVisible: observeVisible } ================================================ FILE: miniprogram/components/cover-page/cover-page.wxss ================================================ .action-sheet-backdrop { position: fixed; top: 0; left: 0; z-index: 11; width: 100%; height: 100%; background-color: rgba(0,0,0,.4); pointer-events: none; opacity: 0; transition: opacity .3s; } .action-sheet-wrapper { position: absolute; bottom: 0; left: 0; right: 0; width: 100%; max-height: 100vh; border-radius: 40rpx 40rpx 0 0; overflow: hidden; transform: translate3d(0, 100%, 0); transition: transform .3s; display: flex; flex-direction: column; background-color: #fff; } .action-sheet__title { background-color: #fff; color: #333; font-size: 28rpx; padding: 26rpx 0; text-align: center; } .action-sheet-backdrop.is-show { pointer-events: auto; opacity: 1; } .action-sheet-backdrop.is-show .action-sheet-wrapper { transform: translate3d(0, 0, 0); } .cover-page__content { flex: 1; } .cover-page__scroll { max-height: 80vh; } ================================================ FILE: miniprogram/components/index-list/components/content/content.js ================================================ Component({ options: { addGlobalClass: true }, properties: { data: { type: Object, value: null } } }); ================================================ FILE: miniprogram/components/index-list/components/content/content.json ================================================ { "usingComponents": {} } ================================================ FILE: miniprogram/components/index-list/components/content/content.wxml ================================================ {{data.name}} ================================================ FILE: miniprogram/components/index-list/components/content/content.wxss ================================================ ================================================ FILE: miniprogram/components/index-list/index-list.js ================================================ const throttle = function(func, wait, options) { let context; let args; let result let timeout = null // 上次执行时间点 let previous = 0 if (!options) options = {} // 延迟执行函数 const later = function() { // 若设定了开始边界不执行选项,上次执行时间始终为0 previous = options.leading === false ? 0 : Date.now() timeout = null result = func.apply(context, args) if (!timeout) context = args = null } return function() { const now = Date.now() // 首次执行时,如果设定了开始边界不执行选项,将上次执行时间设定为当前时间。 if (!previous && options.leading === false) previous = now // 延迟执行时间间隔 const remaining = wait - (now - previous) context = this args = arguments // 延迟时间间隔remaining小于等于0,表示上次执行至此所间隔时间已经超过一个时间窗口 // remaining大于时间窗口wait,表示客户端系统时间被调整过 if (remaining <= 0 || remaining > wait) { clearTimeout(timeout) timeout = null previous = now result = func.apply(context, args) if (!timeout) context = args = null // 如果延迟执行不存在,且没有设定结尾边界不执行选项 } else if (!timeout && options.trailing !== false) { timeout = setTimeout(later, remaining) } return result } } Component({ options: { addGlobalClass: true, pureDataPattern: /^_/ // 指定所有 _ 开头的数据字段为纯数据字段 }, properties: { list: { type: Array, value: [], observer: function(newVal) { if (newVal.length === 0) return const data = this.data const alphabet = data.list.map(item => ({ alpha: item.alpha, anchor: item.anchor })) this.setData({ alphabet, current: alphabet[0].anchor }, () => { this.computedSize() }) } }, vibrated: { type: Boolean, value: true } }, data: { windowHeight: 612, current: 'A', intoView: '', touching: false, alphabet: [], _tops: [], _anchorItemH: 0, _anchorItemW: 0, _anchorTop: 0, _listUpperBound: 0 }, lifetimes: { attached() { this.__scrollTo = throttle(this._scrollTo, 100, {}) this.__onScroll = throttle(this._onScroll, 100, {}) const {windowHeight} = wx.getSystemInfoSync() this.setData({windowHeight}) } }, methods: { choose (e) { const item = e.currentTarget.dataset.item; this.triggerEvent('choose', { item }); }, scrollTo(e) { this.__scrollTo(e) }, _scrollTo(e) { const data = this.data const clientY = e.changedTouches[0].clientY const index = Math.floor((clientY - data._anchorTop) / data._anchorItemH) const current = data.alphabet[index]?.anchor if (current && current !== data.current) { this.setData({current, intoView: current, touching: true}) // 振动效果 if (data.vibrated) wx.vibrateShort() } }, computedSize() { const data = this.data // 计算列表每个区块的高度等信息 const query = this.createSelectorQuery() query.selectAll('.index_list_item').boundingClientRect(rects => { const result = rects data._tops = result.map(item => item.top) }).exec() // 计算右侧字母栏小区块的高度等信息 query.select('.anchor-list').boundingClientRect(rect => { data._anchorItemH = rect.height / data.alphabet.length data._anchorItemW = rect.width data._anchorTop = rect.top }).exec() // 计算滚动区域的上边界 query.select('.page-select-index').boundingClientRect(rect => { data._listUpperBound = rect.top }) }, // throttle 的延迟 removeTouching() { setTimeout(() => { this.setData({touching: false}) }, 150) }, onScroll(e) { this.__onScroll(e) }, _onScroll(e) { const data = this.data const {_tops, alphabet} = data const scrollTop = e.detail.scrollTop let current = '' if (scrollTop < _tops[0]) { current = alphabet[0].anchor } else { for (let i = 0, len = _tops.length; i < len - 1; i++) { if (scrollTop >= _tops[i] && scrollTop < _tops[i + 1]) { current = alphabet[i].anchor } } } if (!current) current = alphabet[alphabet.length - 1].anchor this.setData({current}) } } }) ================================================ FILE: miniprogram/components/index-list/index-list.json ================================================ { "component": true, "usingComponents": {}, "componentGenerics": { "content": { "default": "./components/content/content" } } } ================================================ FILE: miniprogram/components/index-list/index-list.wxml ================================================ {{item.alpha}} {{item.alpha}} {{item.alpha}} ================================================ FILE: miniprogram/components/index-list/index-list.wxss ================================================ .wx-flex{ display: flex; align-items: center; } .wx-flex__item{ flex: 1; } .thin-border-bottom{ position: relative; } .thin-border-bottom:after{ content: ""; position: absolute; left: 0; bottom: 0; right: 0; height: 1px; border-bottom: 1px solid #EAEAEA; color: #e5e5e5; -webkit-transform-origin: 0 0; transform-origin: 0 100%; transform: scaleY(0.5); } .index-group__title{ padding: 12rpx 24rpx; background-color: #ddd; } .index-group__content{ font-size: 0; } .index-group__item{ padding: 30rpx 24rpx; font-size: 30rpx; } .index-group__item.thin-border-bottom:after{ left: 24rpx; } .index-group__item:last-child::after { content: none; } .anchor-bar__wrp{ position: fixed; top: 0; bottom: 0; right: 0; width: 60rpx; z-index: 999; } .anchor-item{ font-size: 0; text-align: center; position: relative; } .anchor-item__inner{ line-height: 28rpx; height: 28rpx; width: 28rpx; border-radius: 50%; display: inline-block; font-size: 20rpx; margin: 2rpx 0; font-weight: 500; } .tapped .anchor-item__pop{ display: block; } .anchor-item__pop{ position: absolute; font-size: 64rpx; width: 100rpx; height: 100rpx; line-height: 100rpx; color: #fff; background-color: #C9C9C9; border-radius: 50%; right: 80rpx; top: 50%; transform: translateY(-50%); display: none; } .anchor-item__pop:after{ content: ""; display: block; position: absolute; width: 0; height: 0; left: 80rpx; border: 40rpx solid; border-color: transparent transparent transparent #C9C9C9; top: 50%; transform: translateY(-50%); } .anchor-item.selected .anchor-item__inner{ color: #fff; background-color: #1aad19; } ================================================ FILE: miniprogram/components/load-more/load-more.js ================================================ Component({ options: { addGlobalClass: true }, properties: { loading: Boolean }, attached() { this.observe() }, detached() { this.observer && this.observer.disconnect() }, methods: { observe() { this.observer = this.createIntersectionObserver({ thresholds: [0], initialRatio: 1 }) this.observer .relativeToViewport({ bottom: 60 }) .observe('.load-more', (res) => { if (res.intersectionRatio > 0 && !this.loading) { this.triggerEvent('loadmore') } }) } } }) ================================================ FILE: miniprogram/components/load-more/load-more.json ================================================ { "usingComponents": {} } ================================================ FILE: miniprogram/components/load-more/load-more.wxml ================================================ {{loading ? 'Loading...' : 'Load more'}} ================================================ FILE: miniprogram/components/load-more/load-more.wxss ================================================ .load-more { display: flex; align-items: center; justify-content: center; } ================================================ FILE: miniprogram/components/painter/lib/downloader.js ================================================ /** * LRU 文件存储,使用该 downloader 可以让下载的文件存储在本地,下次进入小程序后可以直接使用 * 详细设计文档可查看 https://juejin.im/post/5b42d3ede51d4519277b6ce3 */ const util = require('./util'); const SAVED_FILES_KEY = 'savedFiles'; const KEY_TOTAL_SIZE = 'totalSize'; const KEY_PATH = 'path'; const KEY_TIME = 'time'; const KEY_SIZE = 'size'; // 可存储总共为 6M,目前小程序可允许的最大本地存储为 10M let MAX_SPACE_IN_B = 6 * 1024 * 1024; let savedFiles = {}; export default class Dowloader { constructor() { // app 如果设置了最大存储空间,则使用 app 中的 if (getApp().PAINTER_MAX_LRU_SPACE) { MAX_SPACE_IN_B = getApp().PAINTER_MAX_LRU_SPACE; } wx.getStorage({ key: SAVED_FILES_KEY, success: function (res) { if (res.data) { savedFiles = res.data; } }, }); } /** * 下载文件,会用 lru 方式来缓存文件到本地 * @param {String} url 文件的 url */ download(url) { return new Promise((resolve, reject) => { if (!(url && util.isValidUrl(url))) { resolve(url); return; } const file = getFile(url); if (file) { // 检查文件是否正常,不正常需要重新下载 wx.getSavedFileInfo({ filePath: file[KEY_PATH], success: (res) => { resolve(file[KEY_PATH]); }, fail: (error) => { console.error(`the file is broken, redownload it, ${JSON.stringify(error)}`); downloadFile(url).then((path) => { resolve(path); }, () => { reject(); }); }, }); } else { downloadFile(url).then((path) => { resolve(path); }, () => { reject(); }); } }); } } function downloadFile(url) { return new Promise((resolve, reject) => { wx.downloadFile({ url: url, success: function (res) { if (res.statusCode !== 200) { console.error(`downloadFile ${url} failed res.statusCode is not 200`); reject(); return; } const { tempFilePath } = res; wx.getFileInfo({ filePath: tempFilePath, success: (tmpRes) => { const newFileSize = tmpRes.size; doLru(newFileSize).then(() => { saveFile(url, newFileSize, tempFilePath).then((filePath) => { resolve(filePath); }); }, () => { resolve(tempFilePath); }); }, fail: (error) => { // 文件大小信息获取失败,则此文件也不要进行存储 console.error(`getFileInfo ${res.tempFilePath} failed, ${JSON.stringify(error)}`); resolve(res.tempFilePath); }, }); }, fail: function (error) { console.error(`downloadFile failed, ${JSON.stringify(error)} `); reject(); }, }); }); } function saveFile(key, newFileSize, tempFilePath) { return new Promise((resolve, reject) => { wx.saveFile({ tempFilePath: tempFilePath, success: (fileRes) => { const totalSize = savedFiles[KEY_TOTAL_SIZE] ? savedFiles[KEY_TOTAL_SIZE] : 0; savedFiles[key] = {}; savedFiles[key][KEY_PATH] = fileRes.savedFilePath; savedFiles[key][KEY_TIME] = new Date().getTime(); savedFiles[key][KEY_SIZE] = newFileSize; savedFiles['totalSize'] = newFileSize + totalSize; wx.setStorage({ key: SAVED_FILES_KEY, data: savedFiles, }); resolve(fileRes.savedFilePath); }, fail: (error) => { console.error(`saveFile ${key} failed, then we delete all files, ${JSON.stringify(error)}`); // 由于 saveFile 成功后,res.tempFilePath 处的文件会被移除,所以在存储未成功时,我们还是继续使用临时文件 resolve(tempFilePath); // 如果出现错误,就直接情况本地的所有文件,因为你不知道是不是因为哪次lru的某个文件未删除成功 reset(); }, }); }); } /** * 清空所有下载相关内容 */ function reset() { wx.removeStorage({ key: SAVED_FILES_KEY, success: () => { wx.getSavedFileList({ success: (listRes) => { removeFiles(listRes.fileList); }, fail: (getError) => { console.error(`getSavedFileList failed, ${JSON.stringify(getError)}`); }, }); }, }); } function doLru(size) { return new Promise((resolve, reject) => { let totalSize = savedFiles[KEY_TOTAL_SIZE] ? savedFiles[KEY_TOTAL_SIZE] : 0; if (size + totalSize <= MAX_SPACE_IN_B) { resolve(); return; } // 如果加上新文件后大小超过最大限制,则进行 lru const pathsShouldDelete = []; // 按照最后一次的访问时间,从小到大排序 const allFiles = JSON.parse(JSON.stringify(savedFiles)); delete allFiles[KEY_TOTAL_SIZE]; const sortedKeys = Object.keys(allFiles).sort((a, b) => { return allFiles[a][KEY_TIME] - allFiles[b][KEY_TIME]; }); for (const sortedKey of sortedKeys) { totalSize -= savedFiles[sortedKey].size; pathsShouldDelete.push(savedFiles[sortedKey][KEY_PATH]); delete savedFiles[sortedKey]; if (totalSize + size < MAX_SPACE_IN_B) { break; } } savedFiles['totalSize'] = totalSize; wx.setStorage({ key: SAVED_FILES_KEY, data: savedFiles, success: () => { // 保证 storage 中不会存在不存在的文件数据 if (pathsShouldDelete.length > 0) { removeFiles(pathsShouldDelete); } resolve(); }, fail: (error) => { console.error(`doLru setStorage failed, ${JSON.stringify(error)}`); reject(); }, }); }); } function removeFiles(pathsShouldDelete) { for (const pathDel of pathsShouldDelete) { let delPath = pathDel; if (typeof pathDel === 'object') { delPath = pathDel.filePath; } wx.removeSavedFile({ filePath: delPath, fail: (error) => { console.error(`removeSavedFile ${pathDel} failed, ${JSON.stringify(error)}`); }, }); } } function getFile(key) { if (!savedFiles[key]) { return; } savedFiles[key]['time'] = new Date().getTime(); wx.setStorage({ key: SAVED_FILES_KEY, data: savedFiles, }); return savedFiles[key]; } ================================================ FILE: miniprogram/components/painter/lib/gradient.js ================================================ /* eslint-disable */ // 当ctx传入当前文件,const grd = ctx.createCircularGradient() 和 // const grd = this.ctx.createLinearGradient() 无效,因此只能分开处理 // 先分析,在外部创建grd,再传入使用就可以 !(function () { var api = { isGradient: function(bg) { if (bg && (bg.startsWith('linear') || bg.startsWith('radial'))) { return true; } return false; }, doGradient: function(bg, width, height, ctx) { if (bg.startsWith('linear')) { linearEffect(width, height, bg, ctx); } else if (bg.startsWith('radial')) { radialEffect(width, height, bg, ctx); } }, } function analizeGrad(string) { const colorPercents = string.substring(0, string.length - 1).split("%,"); const colors = []; const percents = []; for (let colorPercent of colorPercents) { colors.push(colorPercent.substring(0, colorPercent.lastIndexOf(" ")).trim()); percents.push(colorPercent.substring(colorPercent.lastIndexOf(" "), colorPercent.length) / 100); } return {colors: colors, percents: percents}; } function radialEffect(width, height, bg, ctx) { const colorPer = analizeGrad(bg.match(/radial-gradient\((.+)\)/)[1]); const grd = ctx.createCircularGradient(0, 0, width < height ? height / 2 : width / 2); for (let i = 0; i < colorPer.colors.length; i++) { grd.addColorStop(colorPer.percents[i], colorPer.colors[i]); } ctx.fillStyle = grd; //ctx.fillRect(-(width / 2), -(height / 2), width, height); } function analizeLinear(bg, width, height) { const direction = bg.match(/([-]?\d{1,3})deg/); const dir = direction && direction[1] ? parseFloat(direction[1]) : 0; let coordinate; switch (dir) { case 0: coordinate = [0, -height / 2, 0, height / 2]; break; case 90: coordinate = [width / 2, 0, -width / 2, 0]; break; case -90: coordinate = [-width / 2, 0, width / 2, 0]; break; case 180: coordinate = [0, height / 2, 0, -height / 2]; break; case -180: coordinate = [0, -height / 2, 0, height / 2]; break; default: let x1 = 0; let y1 = 0; let x2 = 0; let y2 = 0; if (direction[1] > 0 && direction[1] < 90) { x1 = (width / 2) - ((width / 2) * Math.tan((90 - direction[1]) * Math.PI * 2 / 360) - height / 2) * Math.sin(2 * (90 - direction[1]) * Math.PI * 2 / 360) / 2; y2 = Math.tan((90 - direction[1]) * Math.PI * 2 / 360) * x1; x2 = -x1; y1 = -y2; } else if (direction[1] > -180 && direction[1] < -90) { x1 = -(width / 2) + ((width / 2) * Math.tan((90 - direction[1]) * Math.PI * 2 / 360) - height / 2) * Math.sin(2 * (90 - direction[1]) * Math.PI * 2 / 360) / 2; y2 = Math.tan((90 - direction[1]) * Math.PI * 2 / 360) * x1; x2 = -x1; y1 = -y2; } else if (direction[1] > 90 && direction[1] < 180) { x1 = (width / 2) + (-(width / 2) * Math.tan((90 - direction[1]) * Math.PI * 2 / 360) - height / 2) * Math.sin(2 * (90 - direction[1]) * Math.PI * 2 / 360) / 2; y2 = Math.tan((90 - direction[1]) * Math.PI * 2 / 360) * x1; x2 = -x1; y1 = -y2; } else { x1 = -(width / 2) - (-(width / 2) * Math.tan((90 - direction[1]) * Math.PI * 2 / 360) - height / 2) * Math.sin(2 * (90 - direction[1]) * Math.PI * 2 / 360) / 2; y2 = Math.tan((90 - direction[1]) * Math.PI * 2 / 360) * x1; x2 = -x1; y1 = -y2; } coordinate = [x1, y1, x2, y2]; break; } return coordinate; } function linearEffect(width, height, bg, ctx) { const param = analizeLinear(bg, width, height); const grd = ctx.createLinearGradient(param[0], param[1], param[2], param[3]); const content = bg.match(/linear-gradient\((.+)\)/)[1]; const colorPer = analizeGrad(content.substring(content.indexOf(',') + 1)); for (let i = 0; i < colorPer.colors.length; i++) { grd.addColorStop(colorPer.percents[i], colorPer.colors[i]); } ctx.fillStyle = grd //ctx.fillRect(-(width / 2), -(height / 2), width, height); } module.exports = { api } })(); ================================================ FILE: miniprogram/components/painter/lib/pen.js ================================================ const QR = require('./qrcode.js'); const GD = require('./gradient.js'); export default class Painter { constructor(ctx, data) { this.ctx = ctx; this.data = data; /** * 记录元素信息 * @type {{ * [indexOrId: string]: { * width: number * height: number * left: number * top: number * } * }} */ this.elements = {}; this.globalWidth = {}; this.globalHeight = {}; } paint(callback) { this.style = { width: this.data.width.toPx(), height: this.data.height.toPx(), }; this._background(); const views = this.data.views || []; views.forEach((view) => this._drawAbsolute(view)) this.ctx.draw(false, () => { callback(); }); } _background() { this.ctx.save(); const { width, height, } = this.style; const bg = this.data.background; this.ctx.translate(width / 2, height / 2); this._doClip(this.data.borderRadius, width, height); if (!bg) { // 如果未设置背景,则默认使用白色 this.ctx.fillStyle = '#fff'; this.ctx.fillRect(-(width / 2), -(height / 2), width, height); } else if (bg.startsWith('#') || bg.startsWith('rgba') || bg.toLowerCase() === 'transparent') { // 背景填充颜色 this.ctx.fillStyle = bg; this.ctx.fillRect(-(width / 2), -(height / 2), width, height); } else if (GD.api.isGradient(bg)) { GD.api.doGradient(bg, width, height, this.ctx); this.ctx.fillRect(-(width / 2), -(height / 2), width, height); } else { // 背景填充图片 this.ctx.drawImage(bg, -(width / 2), -(height / 2), width, height); } this.ctx.restore(); } _drawAbsolute(view) { if (!view) { return } // 证明 css 为数组形式,需要合并 if (view.css && view.css.length) { /* eslint-disable no-param-reassign */ view.css = Object.assign(...view.css); } switch (view.type) { case 'image': this._drawAbsImage(view); break; case 'text': this._fillAbsText(view); break; case 'rect': this._drawAbsRect(view); break; case 'qrcode': this._drawQRCode(view); break; default: break; } } /** * 根据 borderRadius 进行裁减 */ _doClip(borderRadius, width, height) { if (borderRadius && width && height) { // radius 改为4个值,分别对应4个角 let radius = borderRadius.trim().split(' '); switch (radius.length) { case 1: radius.length = 4; radius.fill(radius[0], 1, 4); break; default: radius[2] = radius[2] || radius[0]; radius[3] = radius[3] || radius[1]; } // 转换 px 单位,只保留数字 radius = radius.map((v) => Math.min(v.toPx(), width / 2, height / 2)); // 防止在某些机型上周边有黑框现象,此处如果直接设置 fillStyle 为透明,在 Android 机型上会导致被裁减的图片也变为透明, iOS 和 IDE 上不会 // globalAlpha 在 1.9.90 起支持,低版本下无效,但把 fillStyle 设为了 white,相对默认的 black 要好点 this.ctx.globalAlpha = 0; this.ctx.fillStyle = 'white'; this.ctx.beginPath(); this.ctx.arc(-width / 2 + radius[0], -height / 2 + radius[0], radius[0], 1 * Math.PI, 1.5 * Math.PI); this.ctx.lineTo(width / 2 - radius[1], -height / 2); this.ctx.arc(width / 2 - radius[1], -height / 2 + radius[1], radius[1], 1.5 * Math.PI, 2 * Math.PI); this.ctx.lineTo(width / 2, height / 2 - radius[2]); this.ctx.arc(width / 2 - radius[2], height / 2 - radius[2], radius[2], 0, 0.5 * Math.PI); this.ctx.lineTo(-width / 2 + radius[3], height / 2); this.ctx.arc(-width / 2 + radius[3], height / 2 - radius[3], radius[3], 0.5 * Math.PI, 1 * Math.PI); this.ctx.closePath(); this.ctx.fill(); // 在 ios 的 6.6.6 版本上 clip 有 bug,禁掉此类型上的 clip,也就意味着,在此版本微信的 ios 设备下无法使用 border 属性 if (!(getApp().systemInfo && getApp().systemInfo.version <= '6.6.6' && getApp().systemInfo.platform === 'ios')) { this.ctx.clip(); } this.ctx.globalAlpha = 1; } } /** * 画边框 */ _doBorder(view, width, height) { if (!view.css) { return; } const { borderRadius, borderWidth, borderColor, } = view.css; if (!borderWidth) { return; } this.ctx.save(); this._preProcess({ view, notClip: true }); let r; if (borderRadius) { r = Math.min(borderRadius.toPx(), width / 2, height / 2); } else { r = 0; } const lineWidth = borderWidth.toPx(); this.ctx.lineWidth = lineWidth; this.ctx.strokeStyle = (borderColor || 'black'); this.ctx.beginPath(); this.ctx.arc(-width / 2 + r, -height / 2 + r, r + lineWidth / 2, 1 * Math.PI, 1.5 * Math.PI); this.ctx.lineTo(width / 2 - r, -height / 2 - lineWidth / 2); this.ctx.arc(width / 2 - r, -height / 2 + r, r + lineWidth / 2, 1.5 * Math.PI, 2 * Math.PI); this.ctx.lineTo(width / 2 + lineWidth / 2, height / 2 - r); this.ctx.arc(width / 2 - r, height / 2 - r, r + lineWidth / 2, 0, 0.5 * Math.PI); this.ctx.lineTo(-width / 2 + r, height / 2 + lineWidth / 2); this.ctx.arc(-width / 2 + r, height / 2 - r, r + lineWidth / 2, 0.5 * Math.PI, 1 * Math.PI); this.ctx.closePath(); this.ctx.stroke(); this.ctx.restore(); } /** * * @param {Object} params * @param {Object} params.view * @param {boolean} [params.notClip] */ _preProcess ({ view, notClip }) { let width = 0; let height; let extra; switch (view.type) { case 'text': { const textArray = view.text.split('\n'); // 处理多个连续的'\n' for (let i = 0; i < textArray.length; ++i) { if (textArray[i] === '') { textArray[i] = ' '; } } const fontWeight = view.css.fontWeight === 'bold' ? 'bold' : 'normal'; view.css.fontSize = view.css.fontSize ? view.css.fontSize : '20rpx'; this.ctx.font = `normal ${fontWeight} ${view.css.fontSize.toPx()}px ${view.css.fontFamily ? view.css.fontFamily : 'sans-serif'}`; // this.ctx.setFontSize(view.css.fontSize.toPx()); // 计算行数 let lines = 0; const linesArray = []; for (let i = 0; i < textArray.length; ++i) { const textLength = this.ctx.measureText(textArray[i]).width; const partWidth = view.css.width ? view.css.width.toPx() : textLength; const calLines = Math.ceil(textLength / partWidth); width = partWidth > width ? partWidth : width; lines += calLines; linesArray[i] = calLines; } lines = view.css.maxLines < lines ? view.css.maxLines : lines; const lineHeight = view.css.lineHeight ? view.css.lineHeight.toPx() : view.css.fontSize.toPx(); height = lineHeight * lines; extra = { lines: lines, lineHeight: lineHeight, textArray: textArray, linesArray: linesArray, }; break; } case 'image': { // image的长宽设置成auto的逻辑处理 const ratio = getApp().systemInfo.pixelRatio ? getApp().systemInfo.pixelRatio : 2; // 有css却未设置width或height,则默认为auto if (view.css) { if (!view.css.width) { view.css.width = 'auto'; } if (!view.css.height) { view.css.height = 'auto'; } } if (!view.css || (view.css.width === 'auto' && view.css.height === 'auto')) { width = Math.round(view.sWidth / ratio); height = Math.round(view.sHeight / ratio); } else if (view.css.width === 'auto') { height = view.css.height.toPx(); width = view.sWidth / view.sHeight * height; } else if (view.css.height === 'auto') { width = view.css.width.toPx(); height = view.sHeight / view.sWidth * width; } else { width = view.css.width.toPx(); height = view.css.height.toPx(); } break; } default: if (!(view.css.width && view.css.height)) { console.error('You should set width and height'); return; } width = view.css.width.toPx(); height = view.css.height.toPx(); break; } let x; if (view.css && view.css.right) { if (typeof view.css.right === 'string') { x = this.style.width - view.css.right.toPx(true); } else { // 可以用数组方式,把文字长度计算进去 // [right, 文字id, 乘数(默认 1)] const rights = view.css.right; // x = relativeElX + relativeWidth * percent + gap - elWidth x = this.style.width - rights[0].toPx(true) - this.globalWidth[rights[1]] * (rights[2] || 1); } } else if (view.css && view.css.left) { if (typeof view.css.left === 'string') { x = view.css.left.toPx(true); } else { const lefts = view.css.left; // x = relativeElX + relativeWidth * percent + gap // x = lefts[0].toPx(true) + this.globalWidth[lefts[1]] * (lefts[2] || 1); const relativeEl = this.elements[lefts[1]]; x = relativeEl.left + relativeEl.width * (lefts[2] === undefined ? 1 : lefts[2]) + lefts[0].toPx(true); } } else { x = 0; } //const y = view.css && view.css.bottom ? this.style.height - height - view.css.bottom.toPx(true) : (view.css && view.css.top ? view.css.top.toPx(true) : 0); let y; if (view.css && view.css.bottom) { y = this.style.height - height - view.css.bottom.toPx(true); } else { if (view.css && view.css.top) { if (typeof view.css.top === 'string') { y = view.css.top.toPx(true); } else { const tops = view.css.top; // y = relativeElY + relativeElHeight * percent + gap // y = tops[0].toPx(true) + this.globalHeight[tops[1]] * (tops[2] || 1); const relativeEl = this.elements[tops[1]]; y = relativeEl.top + tops[0].toPx(true) + relativeEl.height * (tops[2] === undefined ? 1 : tops[2]); } } else { y = 0 } } const angle = view.css && view.css.rotate ? this._getAngle(view.css.rotate) : 0; // 当设置了 right 时,默认 align 用 right,反之用 left const align = view.css && view.css.align ? view.css.align : (view.css && view.css.right ? 'right' : 'left'); switch (align) { case 'center': this.ctx.translate(x, y + height / 2); break; case 'right': this.ctx.translate(x - width / 2, y + height / 2); break; default: this.ctx.translate(x + width / 2, y + height / 2); break; } this.ctx.rotate(angle); if (!notClip && view.css && view.css.borderRadius && view.type !== 'rect') { this._doClip(view.css.borderRadius, width, height); } this._doShadow(view); const elInfo = { x, y, width, height, left: x, top: y, extra }; if (view.id) { this.elements[view.id] = elInfo; this.globalWidth[view.id] = width; this.globalHeight[view.id] = height; } return elInfo; } // 画文字的背景图片 _doBackground(view) { this.ctx.save(); const { width: rawWidth, height: rawHeight, } = this._preProcess({ view, notClip: true }); const { background, padding, } = view.css; let pd = [0, 0, 0, 0]; if (padding) { const pdg = padding.split(/\s+/); if (pdg.length === 1) { const x = pdg[0].toPx(); pd = [x, x, x, x]; } if (pdg.length === 2) { const x = pdg[0].toPx(); const y = pdg[1].toPx(); pd = [x, y, x, y]; } if (pdg.length === 3) { const x = pdg[0].toPx(); const y = pdg[1].toPx(); const z = pdg[2].toPx(); pd = [x, y, z, y]; } if (pdg.length === 4) { const x = pdg[0].toPx(); const y = pdg[1].toPx(); const z = pdg[2].toPx(); const a = pdg[3].toPx(); pd = [x, y, z, a]; } } const width = rawWidth + pd[1] + pd[3]; const height = rawHeight + pd[0] + pd[2]; this._doClip(view.css.borderRadius, width, height) if (GD.api.isGradient(background)) { GD.api.doGradient(background, width, height, this.ctx); } else { this.ctx.fillStyle = background; } this.ctx.fillRect(-(width / 2), -(height / 2), width, height); this.ctx.restore(); } _drawQRCode(view) { this.ctx.save(); const { width, height, } = this._preProcess({ view }); QR.api.draw(view.content, this.ctx, -width / 2, -height / 2, width, height, view.css.background, view.css.color); this.ctx.restore(); this._doBorder(view, width, height); } _drawAbsImage(view) { if (!view.url) { return; } this.ctx.save(); const { width, height, } = this._preProcess({ view }); // 获得缩放到图片大小级别的裁减框 let rWidth = view.sWidth; let rHeight = view.sHeight; let startX = 0; let startY = 0; // 绘画区域比例 const cp = width / height; // 原图比例 const op = view.sWidth / view.sHeight; if (cp >= op) { rHeight = rWidth / cp; startY = Math.round((view.sHeight - rHeight) / 2); } else { rWidth = rHeight * cp; startX = Math.round((view.sWidth - rWidth) / 2); } if (view.css && view.css.mode === 'scaleToFill') { this.ctx.drawImage(view.url, -(width / 2), -(height / 2), width, height); } else { this.ctx.drawImage(view.url, startX, startY, rWidth, rHeight, -(width / 2), -(height / 2), width, height); } this.ctx.restore(); this._doBorder(view, width, height); } _fillAbsText(view) { if (!view.text) { return; } if (view.css.background) { // 生成背景 this._doBackground(view); } this.ctx.save(); const { width, height, extra, } = this._preProcess({ view, notClip: view.css.background && view.css.borderRadius }); this.ctx.fillStyle = (view.css.color || 'black'); const { lines, lineHeight, textArray, linesArray, } = extra; // 如果设置了id,则保留 text 的长度 if (view.id) { let textWidth = 0; for (let i = 0; i < textArray.length; ++i) { textWidth = this.ctx.measureText(textArray[i]).width > textWidth ? this.ctx.measureText(textArray[i]).width : textWidth; } this.globalWidth[view.id] = width ? (textWidth < width ? textWidth : width) : textWidth; } let lineIndex = 0; for (let j = 0; j < textArray.length; ++j) { const preLineLength = Math.round(textArray[j].length / linesArray[j]); let start = 0; let alreadyCount = 0; for (let i = 0; i < linesArray[j]; ++i) { // 绘制行数大于最大行数,则直接跳出循环 if (lineIndex >= lines) { break; } alreadyCount = preLineLength; let text = textArray[j].substr(start, alreadyCount); let measuredWith = this.ctx.measureText(text).width; // 如果测量大小小于width一个字符的大小,则进行补齐,如果测量大小超出 width,则进行减除 // 如果已经到文本末尾,也不要进行该循环 while ((start + alreadyCount <= textArray[j].length) && (width - measuredWith > view.css.fontSize.toPx() || measuredWith > width)) { if (measuredWith < width) { text = textArray[j].substr(start, ++alreadyCount); } else { if (text.length <= 1) { // 如果只有一个字符时,直接跳出循环 break; } text = textArray[j].substr(start, --alreadyCount); } measuredWith = this.ctx.measureText(text).width; } start += text.length; // 如果是最后一行了,发现还有未绘制完的内容,则加... if (lineIndex === lines - 1 && (j < textArray.length - 1 || start < textArray[j].length)) { while (this.ctx.measureText(`${text}...`).width > width) { if (text.length <= 1) { // 如果只有一个字符时,直接跳出循环 break; } text = text.substring(0, text.length - 1); } text += '...'; measuredWith = this.ctx.measureText(text).width; } this.ctx.setTextAlign(view.css.textAlign ? view.css.textAlign : 'left'); let x; switch (view.css.textAlign) { case 'center': x = 0; break; case 'right': x = (width / 2); break; default: x = -(width / 2); break; } const y = -(height / 2) + (lineIndex === 0 ? view.css.fontSize.toPx() : (view.css.fontSize.toPx() + lineIndex * lineHeight)); lineIndex++; if (view.css.textStyle === 'stroke') { this.ctx.strokeText(text, x, y, measuredWith); } else { this.ctx.fillText(text, x, y, measuredWith); } const fontSize = view.css.fontSize.toPx(); if (view.css.textDecoration) { this.ctx.beginPath(); if (/\bunderline\b/.test(view.css.textDecoration)) { this.ctx.moveTo(x, y); this.ctx.lineTo(x + measuredWith, y); } if (/\boverline\b/.test(view.css.textDecoration)) { this.ctx.moveTo(x, y - fontSize); this.ctx.lineTo(x + measuredWith, y - fontSize); } if (/\bline-through\b/.test(view.css.textDecoration)) { this.ctx.moveTo(x, y - fontSize / 3); this.ctx.lineTo(x + measuredWith, y - fontSize / 3); } this.ctx.closePath(); this.ctx.strokeStyle = view.css.color; this.ctx.stroke(); } } } this.ctx.restore(); this._doBorder(view, width, height); } _drawAbsRect(view) { this.ctx.save(); const { width, height, } = this._preProcess({ view }); if (GD.api.isGradient(view.css.color)) { GD.api.doGradient(view.css.color, width, height, this.ctx); } else { this.ctx.fillStyle = view.css.color; } const borderRadius = view.css.borderRadius const r = borderRadius ? Math.min(borderRadius.toPx(), width / 2, height / 2) : 0; this.ctx.beginPath(); this.ctx.arc(-width / 2 + r, -height / 2 + r, r, 1 * Math.PI, 1.5 * Math.PI); //左上角圆弧 this.ctx.lineTo(width / 2 - r, -height / 2); this.ctx.arc(width / 2 - r, -height / 2 + r, r, 1.5 * Math.PI, 2 * Math.PI); // 右上角圆弧 this.ctx.lineTo(width / 2, height / 2 - r); this.ctx.arc(width / 2 - r, height / 2 - r, r, 0, 0.5 * Math.PI); // 右下角圆弧 this.ctx.lineTo(-width / 2 + r, height / 2); this.ctx.arc(-width / 2 + r, height / 2 - r, r, 0.5 * Math.PI, 1 * Math.PI); // 左下角圆弧 this.ctx.closePath(); this.ctx.fill(); this.ctx.restore(); this._doBorder(view, width, height); } // shadow 支持 (x, y, blur, color), 不支持 spread // shadow:0px 0px 10px rgba(0,0,0,0.1); _doShadow(view) { if (!view.css || !view.css.shadow) { return; } const box = view.css.shadow.replace(/,\s+/g, ',').split(' '); if (box.length > 4) { console.error('shadow don\'t spread option'); return; } this.ctx.shadowOffsetX = parseInt(box[0], 10); this.ctx.shadowOffsetY = parseInt(box[1], 10); this.ctx.shadowBlur = parseInt(box[2], 10); this.ctx.shadowColor = box[3]; } _getAngle(angle) { return Number(angle) * Math.PI / 180; } } ================================================ FILE: miniprogram/components/painter/lib/qrcode.js ================================================ /* eslint-disable */ !(function () { // alignment pattern var adelta = [ 0, 11, 15, 19, 23, 27, 31, 16, 18, 20, 22, 24, 26, 28, 20, 22, 24, 24, 26, 28, 28, 22, 24, 24, 26, 26, 28, 28, 24, 24, 26, 26, 26, 28, 28, 24, 26, 26, 26, 28, 28 ]; // version block var vpat = [ 0xc94, 0x5bc, 0xa99, 0x4d3, 0xbf6, 0x762, 0x847, 0x60d, 0x928, 0xb78, 0x45d, 0xa17, 0x532, 0x9a6, 0x683, 0x8c9, 0x7ec, 0xec4, 0x1e1, 0xfab, 0x08e, 0xc1a, 0x33f, 0xd75, 0x250, 0x9d5, 0x6f0, 0x8ba, 0x79f, 0xb0b, 0x42e, 0xa64, 0x541, 0xc69 ]; // final format bits with mask: level << 3 | mask var fmtword = [ 0x77c4, 0x72f3, 0x7daa, 0x789d, 0x662f, 0x6318, 0x6c41, 0x6976, //L 0x5412, 0x5125, 0x5e7c, 0x5b4b, 0x45f9, 0x40ce, 0x4f97, 0x4aa0, //M 0x355f, 0x3068, 0x3f31, 0x3a06, 0x24b4, 0x2183, 0x2eda, 0x2bed, //Q 0x1689, 0x13be, 0x1ce7, 0x19d0, 0x0762, 0x0255, 0x0d0c, 0x083b //H ]; // 4 per version: number of blocks 1,2; data width; ecc width var eccblocks = [ 1, 0, 19, 7, 1, 0, 16, 10, 1, 0, 13, 13, 1, 0, 9, 17, 1, 0, 34, 10, 1, 0, 28, 16, 1, 0, 22, 22, 1, 0, 16, 28, 1, 0, 55, 15, 1, 0, 44, 26, 2, 0, 17, 18, 2, 0, 13, 22, 1, 0, 80, 20, 2, 0, 32, 18, 2, 0, 24, 26, 4, 0, 9, 16, 1, 0, 108, 26, 2, 0, 43, 24, 2, 2, 15, 18, 2, 2, 11, 22, 2, 0, 68, 18, 4, 0, 27, 16, 4, 0, 19, 24, 4, 0, 15, 28, 2, 0, 78, 20, 4, 0, 31, 18, 2, 4, 14, 18, 4, 1, 13, 26, 2, 0, 97, 24, 2, 2, 38, 22, 4, 2, 18, 22, 4, 2, 14, 26, 2, 0, 116, 30, 3, 2, 36, 22, 4, 4, 16, 20, 4, 4, 12, 24, 2, 2, 68, 18, 4, 1, 43, 26, 6, 2, 19, 24, 6, 2, 15, 28, 4, 0, 81, 20, 1, 4, 50, 30, 4, 4, 22, 28, 3, 8, 12, 24, 2, 2, 92, 24, 6, 2, 36, 22, 4, 6, 20, 26, 7, 4, 14, 28, 4, 0, 107, 26, 8, 1, 37, 22, 8, 4, 20, 24, 12, 4, 11, 22, 3, 1, 115, 30, 4, 5, 40, 24, 11, 5, 16, 20, 11, 5, 12, 24, 5, 1, 87, 22, 5, 5, 41, 24, 5, 7, 24, 30, 11, 7, 12, 24, 5, 1, 98, 24, 7, 3, 45, 28, 15, 2, 19, 24, 3, 13, 15, 30, 1, 5, 107, 28, 10, 1, 46, 28, 1, 15, 22, 28, 2, 17, 14, 28, 5, 1, 120, 30, 9, 4, 43, 26, 17, 1, 22, 28, 2, 19, 14, 28, 3, 4, 113, 28, 3, 11, 44, 26, 17, 4, 21, 26, 9, 16, 13, 26, 3, 5, 107, 28, 3, 13, 41, 26, 15, 5, 24, 30, 15, 10, 15, 28, 4, 4, 116, 28, 17, 0, 42, 26, 17, 6, 22, 28, 19, 6, 16, 30, 2, 7, 111, 28, 17, 0, 46, 28, 7, 16, 24, 30, 34, 0, 13, 24, 4, 5, 121, 30, 4, 14, 47, 28, 11, 14, 24, 30, 16, 14, 15, 30, 6, 4, 117, 30, 6, 14, 45, 28, 11, 16, 24, 30, 30, 2, 16, 30, 8, 4, 106, 26, 8, 13, 47, 28, 7, 22, 24, 30, 22, 13, 15, 30, 10, 2, 114, 28, 19, 4, 46, 28, 28, 6, 22, 28, 33, 4, 16, 30, 8, 4, 122, 30, 22, 3, 45, 28, 8, 26, 23, 30, 12, 28, 15, 30, 3, 10, 117, 30, 3, 23, 45, 28, 4, 31, 24, 30, 11, 31, 15, 30, 7, 7, 116, 30, 21, 7, 45, 28, 1, 37, 23, 30, 19, 26, 15, 30, 5, 10, 115, 30, 19, 10, 47, 28, 15, 25, 24, 30, 23, 25, 15, 30, 13, 3, 115, 30, 2, 29, 46, 28, 42, 1, 24, 30, 23, 28, 15, 30, 17, 0, 115, 30, 10, 23, 46, 28, 10, 35, 24, 30, 19, 35, 15, 30, 17, 1, 115, 30, 14, 21, 46, 28, 29, 19, 24, 30, 11, 46, 15, 30, 13, 6, 115, 30, 14, 23, 46, 28, 44, 7, 24, 30, 59, 1, 16, 30, 12, 7, 121, 30, 12, 26, 47, 28, 39, 14, 24, 30, 22, 41, 15, 30, 6, 14, 121, 30, 6, 34, 47, 28, 46, 10, 24, 30, 2, 64, 15, 30, 17, 4, 122, 30, 29, 14, 46, 28, 49, 10, 24, 30, 24, 46, 15, 30, 4, 18, 122, 30, 13, 32, 46, 28, 48, 14, 24, 30, 42, 32, 15, 30, 20, 4, 117, 30, 40, 7, 47, 28, 43, 22, 24, 30, 10, 67, 15, 30, 19, 6, 118, 30, 18, 31, 47, 28, 34, 34, 24, 30, 20, 61, 15, 30 ]; // Galois field log table var glog = [ 0xff, 0x00, 0x01, 0x19, 0x02, 0x32, 0x1a, 0xc6, 0x03, 0xdf, 0x33, 0xee, 0x1b, 0x68, 0xc7, 0x4b, 0x04, 0x64, 0xe0, 0x0e, 0x34, 0x8d, 0xef, 0x81, 0x1c, 0xc1, 0x69, 0xf8, 0xc8, 0x08, 0x4c, 0x71, 0x05, 0x8a, 0x65, 0x2f, 0xe1, 0x24, 0x0f, 0x21, 0x35, 0x93, 0x8e, 0xda, 0xf0, 0x12, 0x82, 0x45, 0x1d, 0xb5, 0xc2, 0x7d, 0x6a, 0x27, 0xf9, 0xb9, 0xc9, 0x9a, 0x09, 0x78, 0x4d, 0xe4, 0x72, 0xa6, 0x06, 0xbf, 0x8b, 0x62, 0x66, 0xdd, 0x30, 0xfd, 0xe2, 0x98, 0x25, 0xb3, 0x10, 0x91, 0x22, 0x88, 0x36, 0xd0, 0x94, 0xce, 0x8f, 0x96, 0xdb, 0xbd, 0xf1, 0xd2, 0x13, 0x5c, 0x83, 0x38, 0x46, 0x40, 0x1e, 0x42, 0xb6, 0xa3, 0xc3, 0x48, 0x7e, 0x6e, 0x6b, 0x3a, 0x28, 0x54, 0xfa, 0x85, 0xba, 0x3d, 0xca, 0x5e, 0x9b, 0x9f, 0x0a, 0x15, 0x79, 0x2b, 0x4e, 0xd4, 0xe5, 0xac, 0x73, 0xf3, 0xa7, 0x57, 0x07, 0x70, 0xc0, 0xf7, 0x8c, 0x80, 0x63, 0x0d, 0x67, 0x4a, 0xde, 0xed, 0x31, 0xc5, 0xfe, 0x18, 0xe3, 0xa5, 0x99, 0x77, 0x26, 0xb8, 0xb4, 0x7c, 0x11, 0x44, 0x92, 0xd9, 0x23, 0x20, 0x89, 0x2e, 0x37, 0x3f, 0xd1, 0x5b, 0x95, 0xbc, 0xcf, 0xcd, 0x90, 0x87, 0x97, 0xb2, 0xdc, 0xfc, 0xbe, 0x61, 0xf2, 0x56, 0xd3, 0xab, 0x14, 0x2a, 0x5d, 0x9e, 0x84, 0x3c, 0x39, 0x53, 0x47, 0x6d, 0x41, 0xa2, 0x1f, 0x2d, 0x43, 0xd8, 0xb7, 0x7b, 0xa4, 0x76, 0xc4, 0x17, 0x49, 0xec, 0x7f, 0x0c, 0x6f, 0xf6, 0x6c, 0xa1, 0x3b, 0x52, 0x29, 0x9d, 0x55, 0xaa, 0xfb, 0x60, 0x86, 0xb1, 0xbb, 0xcc, 0x3e, 0x5a, 0xcb, 0x59, 0x5f, 0xb0, 0x9c, 0xa9, 0xa0, 0x51, 0x0b, 0xf5, 0x16, 0xeb, 0x7a, 0x75, 0x2c, 0xd7, 0x4f, 0xae, 0xd5, 0xe9, 0xe6, 0xe7, 0xad, 0xe8, 0x74, 0xd6, 0xf4, 0xea, 0xa8, 0x50, 0x58, 0xaf ]; // Galios field exponent table var gexp = [ 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1d, 0x3a, 0x74, 0xe8, 0xcd, 0x87, 0x13, 0x26, 0x4c, 0x98, 0x2d, 0x5a, 0xb4, 0x75, 0xea, 0xc9, 0x8f, 0x03, 0x06, 0x0c, 0x18, 0x30, 0x60, 0xc0, 0x9d, 0x27, 0x4e, 0x9c, 0x25, 0x4a, 0x94, 0x35, 0x6a, 0xd4, 0xb5, 0x77, 0xee, 0xc1, 0x9f, 0x23, 0x46, 0x8c, 0x05, 0x0a, 0x14, 0x28, 0x50, 0xa0, 0x5d, 0xba, 0x69, 0xd2, 0xb9, 0x6f, 0xde, 0xa1, 0x5f, 0xbe, 0x61, 0xc2, 0x99, 0x2f, 0x5e, 0xbc, 0x65, 0xca, 0x89, 0x0f, 0x1e, 0x3c, 0x78, 0xf0, 0xfd, 0xe7, 0xd3, 0xbb, 0x6b, 0xd6, 0xb1, 0x7f, 0xfe, 0xe1, 0xdf, 0xa3, 0x5b, 0xb6, 0x71, 0xe2, 0xd9, 0xaf, 0x43, 0x86, 0x11, 0x22, 0x44, 0x88, 0x0d, 0x1a, 0x34, 0x68, 0xd0, 0xbd, 0x67, 0xce, 0x81, 0x1f, 0x3e, 0x7c, 0xf8, 0xed, 0xc7, 0x93, 0x3b, 0x76, 0xec, 0xc5, 0x97, 0x33, 0x66, 0xcc, 0x85, 0x17, 0x2e, 0x5c, 0xb8, 0x6d, 0xda, 0xa9, 0x4f, 0x9e, 0x21, 0x42, 0x84, 0x15, 0x2a, 0x54, 0xa8, 0x4d, 0x9a, 0x29, 0x52, 0xa4, 0x55, 0xaa, 0x49, 0x92, 0x39, 0x72, 0xe4, 0xd5, 0xb7, 0x73, 0xe6, 0xd1, 0xbf, 0x63, 0xc6, 0x91, 0x3f, 0x7e, 0xfc, 0xe5, 0xd7, 0xb3, 0x7b, 0xf6, 0xf1, 0xff, 0xe3, 0xdb, 0xab, 0x4b, 0x96, 0x31, 0x62, 0xc4, 0x95, 0x37, 0x6e, 0xdc, 0xa5, 0x57, 0xae, 0x41, 0x82, 0x19, 0x32, 0x64, 0xc8, 0x8d, 0x07, 0x0e, 0x1c, 0x38, 0x70, 0xe0, 0xdd, 0xa7, 0x53, 0xa6, 0x51, 0xa2, 0x59, 0xb2, 0x79, 0xf2, 0xf9, 0xef, 0xc3, 0x9b, 0x2b, 0x56, 0xac, 0x45, 0x8a, 0x09, 0x12, 0x24, 0x48, 0x90, 0x3d, 0x7a, 0xf4, 0xf5, 0xf7, 0xf3, 0xfb, 0xeb, 0xcb, 0x8b, 0x0b, 0x16, 0x2c, 0x58, 0xb0, 0x7d, 0xfa, 0xe9, 0xcf, 0x83, 0x1b, 0x36, 0x6c, 0xd8, 0xad, 0x47, 0x8e, 0x00 ]; // Working buffers: // data input and ecc append, image working buffer, fixed part of image, run lengths for badness var strinbuf = [], eccbuf = [], qrframe = [], framask = [], rlens = []; // Control values - width is based on version, last 4 are from table. var version, width, neccblk1, neccblk2, datablkw, eccblkwid; var ecclevel = 2; // set bit to indicate cell in qrframe is immutable. symmetric around diagonal function setmask(x, y) { var bt; if (x > y) { bt = x; x = y; y = bt; } // y*y = 1+3+5... bt = y; bt *= y; bt += y; bt >>= 1; bt += x; framask[bt] = 1; } // enter alignment pattern - black to qrframe, white to mask (later black frame merged to mask) function putalign(x, y) { var j; qrframe[x + width * y] = 1; for (j = -2; j < 2; j++) { qrframe[(x + j) + width * (y - 2)] = 1; qrframe[(x - 2) + width * (y + j + 1)] = 1; qrframe[(x + 2) + width * (y + j)] = 1; qrframe[(x + j + 1) + width * (y + 2)] = 1; } for (j = 0; j < 2; j++) { setmask(x - 1, y + j); setmask(x + 1, y - j); setmask(x - j, y - 1); setmask(x + j, y + 1); } } //======================================================================== // Reed Solomon error correction // exponentiation mod N function modnn(x) { while (x >= 255) { x -= 255; x = (x >> 8) + (x & 255); } return x; } var genpoly = []; // Calculate and append ECC data to data block. Block is in strinbuf, indexes to buffers given. function appendrs(data, dlen, ecbuf, eclen) { var i, j, fb; for (i = 0; i < eclen; i++) strinbuf[ecbuf + i] = 0; for (i = 0; i < dlen; i++) { fb = glog[strinbuf[data + i] ^ strinbuf[ecbuf]]; if (fb != 255) /* fb term is non-zero */ for (j = 1; j < eclen; j++) strinbuf[ecbuf + j - 1] = strinbuf[ecbuf + j] ^ gexp[modnn(fb + genpoly[eclen - j])]; else for (j = ecbuf; j < ecbuf + eclen; j++) strinbuf[j] = strinbuf[j + 1]; strinbuf[ecbuf + eclen - 1] = fb == 255 ? 0 : gexp[modnn(fb + genpoly[0])]; } } //======================================================================== // Frame data insert following the path rules // check mask - since symmetrical use half. function ismasked(x, y) { var bt; if (x > y) { bt = x; x = y; y = bt; } bt = y; bt += y * y; bt >>= 1; bt += x; return framask[bt]; } //======================================================================== // Apply the selected mask out of the 8. function applymask(m) { var x, y, r3x, r3y; switch (m) { case 0: for (y = 0; y < width; y++) for (x = 0; x < width; x++) if (!((x + y) & 1) && !ismasked(x, y)) qrframe[x + y * width] ^= 1; break; case 1: for (y = 0; y < width; y++) for (x = 0; x < width; x++) if (!(y & 1) && !ismasked(x, y)) qrframe[x + y * width] ^= 1; break; case 2: for (y = 0; y < width; y++) for (r3x = 0, x = 0; x < width; x++ , r3x++) { if (r3x == 3) r3x = 0; if (!r3x && !ismasked(x, y)) qrframe[x + y * width] ^= 1; } break; case 3: for (r3y = 0, y = 0; y < width; y++ , r3y++) { if (r3y == 3) r3y = 0; for (r3x = r3y, x = 0; x < width; x++ , r3x++) { if (r3x == 3) r3x = 0; if (!r3x && !ismasked(x, y)) qrframe[x + y * width] ^= 1; } } break; case 4: for (y = 0; y < width; y++) for (r3x = 0, r3y = ((y >> 1) & 1), x = 0; x < width; x++ , r3x++) { if (r3x == 3) { r3x = 0; r3y = !r3y; } if (!r3y && !ismasked(x, y)) qrframe[x + y * width] ^= 1; } break; case 5: for (r3y = 0, y = 0; y < width; y++ , r3y++) { if (r3y == 3) r3y = 0; for (r3x = 0, x = 0; x < width; x++ , r3x++) { if (r3x == 3) r3x = 0; if (!((x & y & 1) + !(!r3x | !r3y)) && !ismasked(x, y)) qrframe[x + y * width] ^= 1; } } break; case 6: for (r3y = 0, y = 0; y < width; y++ , r3y++) { if (r3y == 3) r3y = 0; for (r3x = 0, x = 0; x < width; x++ , r3x++) { if (r3x == 3) r3x = 0; if (!(((x & y & 1) + (r3x && (r3x == r3y))) & 1) && !ismasked(x, y)) qrframe[x + y * width] ^= 1; } } break; case 7: for (r3y = 0, y = 0; y < width; y++ , r3y++) { if (r3y == 3) r3y = 0; for (r3x = 0, x = 0; x < width; x++ , r3x++) { if (r3x == 3) r3x = 0; if (!(((r3x && (r3x == r3y)) + ((x + y) & 1)) & 1) && !ismasked(x, y)) qrframe[x + y * width] ^= 1; } } break; } return; } // Badness coefficients. var N1 = 3, N2 = 3, N3 = 40, N4 = 10; // Using the table of the length of each run, calculate the amount of bad image // - long runs or those that look like finders; called twice, once each for X and Y function badruns(length) { var i; var runsbad = 0; for (i = 0; i <= length; i++) if (rlens[i] >= 5) runsbad += N1 + rlens[i] - 5; // BwBBBwB as in finder for (i = 3; i < length - 1; i += 2) if (rlens[i - 2] == rlens[i + 2] && rlens[i + 2] == rlens[i - 1] && rlens[i - 1] == rlens[i + 1] && rlens[i - 1] * 3 == rlens[i] // white around the black pattern? Not part of spec && (rlens[i - 3] == 0 // beginning || i + 3 > length // end || rlens[i - 3] * 3 >= rlens[i] * 4 || rlens[i + 3] * 3 >= rlens[i] * 4) ) runsbad += N3; return runsbad; } // Calculate how bad the masked image is - blocks, imbalance, runs, or finders. function badcheck() { var x, y, h, b, b1; var thisbad = 0; var bw = 0; // blocks of same color. for (y = 0; y < width - 1; y++) for (x = 0; x < width - 1; x++) if ((qrframe[x + width * y] && qrframe[(x + 1) + width * y] && qrframe[x + width * (y + 1)] && qrframe[(x + 1) + width * (y + 1)]) // all black || !(qrframe[x + width * y] || qrframe[(x + 1) + width * y] || qrframe[x + width * (y + 1)] || qrframe[(x + 1) + width * (y + 1)])) // all white thisbad += N2; // X runs for (y = 0; y < width; y++) { rlens[0] = 0; for (h = b = x = 0; x < width; x++) { if ((b1 = qrframe[x + width * y]) == b) rlens[h]++; else rlens[++h] = 1; b = b1; bw += b ? 1 : -1; } thisbad += badruns(h); } // black/white imbalance if (bw < 0) bw = -bw; var big = bw; var count = 0; big += big << 2; big <<= 1; while (big > width * width) big -= width * width, count++; thisbad += count * N4; // Y runs for (x = 0; x < width; x++) { rlens[0] = 0; for (h = b = y = 0; y < width; y++) { if ((b1 = qrframe[x + width * y]) == b) rlens[h]++; else rlens[++h] = 1; b = b1; } thisbad += badruns(h); } return thisbad; } function genframe(instring) { var x, y, k, t, v, i, j, m; // find the smallest version that fits the string t = instring.length; version = 0; do { version++; k = (ecclevel - 1) * 4 + (version - 1) * 16; neccblk1 = eccblocks[k++]; neccblk2 = eccblocks[k++]; datablkw = eccblocks[k++]; eccblkwid = eccblocks[k]; k = datablkw * (neccblk1 + neccblk2) + neccblk2 - 3 + (version <= 9); if (t <= k) break; } while (version < 40); // FIXME - insure that it fits insted of being truncated width = 17 + 4 * version; // allocate, clear and setup data structures v = datablkw + (datablkw + eccblkwid) * (neccblk1 + neccblk2) + neccblk2; for (t = 0; t < v; t++) eccbuf[t] = 0; strinbuf = instring.slice(0); for (t = 0; t < width * width; t++) qrframe[t] = 0; for (t = 0; t < (width * (width + 1) + 1) / 2; t++) framask[t] = 0; // insert finders - black to frame, white to mask for (t = 0; t < 3; t++) { k = 0; y = 0; if (t == 1) k = (width - 7); if (t == 2) y = (width - 7); qrframe[(y + 3) + width * (k + 3)] = 1; for (x = 0; x < 6; x++) { qrframe[(y + x) + width * k] = 1; qrframe[y + width * (k + x + 1)] = 1; qrframe[(y + 6) + width * (k + x)] = 1; qrframe[(y + x + 1) + width * (k + 6)] = 1; } for (x = 1; x < 5; x++) { setmask(y + x, k + 1); setmask(y + 1, k + x + 1); setmask(y + 5, k + x); setmask(y + x + 1, k + 5); } for (x = 2; x < 4; x++) { qrframe[(y + x) + width * (k + 2)] = 1; qrframe[(y + 2) + width * (k + x + 1)] = 1; qrframe[(y + 4) + width * (k + x)] = 1; qrframe[(y + x + 1) + width * (k + 4)] = 1; } } // alignment blocks if (version > 1) { t = adelta[version]; y = width - 7; for (; ;) { x = width - 7; while (x > t - 3) { putalign(x, y); if (x < t) break; x -= t; } if (y <= t + 9) break; y -= t; putalign(6, y); putalign(y, 6); } } // single black qrframe[8 + width * (width - 8)] = 1; // timing gap - mask only for (y = 0; y < 7; y++) { setmask(7, y); setmask(width - 8, y); setmask(7, y + width - 7); } for (x = 0; x < 8; x++) { setmask(x, 7); setmask(x + width - 8, 7); setmask(x, width - 8); } // reserve mask-format area for (x = 0; x < 9; x++) setmask(x, 8); for (x = 0; x < 8; x++) { setmask(x + width - 8, 8); setmask(8, x); } for (y = 0; y < 7; y++) setmask(8, y + width - 7); // timing row/col for (x = 0; x < width - 14; x++) if (x & 1) { setmask(8 + x, 6); setmask(6, 8 + x); } else { qrframe[(8 + x) + width * 6] = 1; qrframe[6 + width * (8 + x)] = 1; } // version block if (version > 6) { t = vpat[version - 7]; k = 17; for (x = 0; x < 6; x++) for (y = 0; y < 3; y++ , k--) if (1 & (k > 11 ? version >> (k - 12) : t >> k)) { qrframe[(5 - x) + width * (2 - y + width - 11)] = 1; qrframe[(2 - y + width - 11) + width * (5 - x)] = 1; } else { setmask(5 - x, 2 - y + width - 11); setmask(2 - y + width - 11, 5 - x); } } // sync mask bits - only set above for white spaces, so add in black bits for (y = 0; y < width; y++) for (x = 0; x <= y; x++) if (qrframe[x + width * y]) setmask(x, y); // convert string to bitstream // 8 bit data to QR-coded 8 bit data (numeric or alphanum, or kanji not supported) v = strinbuf.length; // string to array for (i = 0; i < v; i++) eccbuf[i] = strinbuf.charCodeAt(i); strinbuf = eccbuf.slice(0); // calculate max string length x = datablkw * (neccblk1 + neccblk2) + neccblk2; if (v >= x - 2) { v = x - 2; if (version > 9) v--; } // shift and repack to insert length prefix i = v; if (version > 9) { strinbuf[i + 2] = 0; strinbuf[i + 3] = 0; while (i--) { t = strinbuf[i]; strinbuf[i + 3] |= 255 & (t << 4); strinbuf[i + 2] = t >> 4; } strinbuf[2] |= 255 & (v << 4); strinbuf[1] = v >> 4; strinbuf[0] = 0x40 | (v >> 12); } else { strinbuf[i + 1] = 0; strinbuf[i + 2] = 0; while (i--) { t = strinbuf[i]; strinbuf[i + 2] |= 255 & (t << 4); strinbuf[i + 1] = t >> 4; } strinbuf[1] |= 255 & (v << 4); strinbuf[0] = 0x40 | (v >> 4); } // fill to end with pad pattern i = v + 3 - (version < 10); while (i < x) { strinbuf[i++] = 0xec; // buffer has room if (i == x) break; strinbuf[i++] = 0x11; } // calculate and append ECC // calculate generator polynomial genpoly[0] = 1; for (i = 0; i < eccblkwid; i++) { genpoly[i + 1] = 1; for (j = i; j > 0; j--) genpoly[j] = genpoly[j] ? genpoly[j - 1] ^ gexp[modnn(glog[genpoly[j]] + i)] : genpoly[j - 1]; genpoly[0] = gexp[modnn(glog[genpoly[0]] + i)]; } for (i = 0; i <= eccblkwid; i++) genpoly[i] = glog[genpoly[i]]; // use logs for genpoly[] to save calc step // append ecc to data buffer k = x; y = 0; for (i = 0; i < neccblk1; i++) { appendrs(y, datablkw, k, eccblkwid); y += datablkw; k += eccblkwid; } for (i = 0; i < neccblk2; i++) { appendrs(y, datablkw + 1, k, eccblkwid); y += datablkw + 1; k += eccblkwid; } // interleave blocks y = 0; for (i = 0; i < datablkw; i++) { for (j = 0; j < neccblk1; j++) eccbuf[y++] = strinbuf[i + j * datablkw]; for (j = 0; j < neccblk2; j++) eccbuf[y++] = strinbuf[(neccblk1 * datablkw) + i + (j * (datablkw + 1))]; } for (j = 0; j < neccblk2; j++) eccbuf[y++] = strinbuf[(neccblk1 * datablkw) + i + (j * (datablkw + 1))]; for (i = 0; i < eccblkwid; i++) for (j = 0; j < neccblk1 + neccblk2; j++) eccbuf[y++] = strinbuf[x + i + j * eccblkwid]; strinbuf = eccbuf; // pack bits into frame avoiding masked area. x = y = width - 1; k = v = 1; // up, minus /* inteleaved data and ecc codes */ m = (datablkw + eccblkwid) * (neccblk1 + neccblk2) + neccblk2; for (i = 0; i < m; i++) { t = strinbuf[i]; for (j = 0; j < 8; j++ , t <<= 1) { if (0x80 & t) qrframe[x + width * y] = 1; do { // find next fill position if (v) x--; else { x++; if (k) { if (y != 0) y--; else { x -= 2; k = !k; if (x == 6) { x--; y = 9; } } } else { if (y != width - 1) y++; else { x -= 2; k = !k; if (x == 6) { x--; y -= 8; } } } } v = !v; } while (ismasked(x, y)); } } // save pre-mask copy of frame strinbuf = qrframe.slice(0); t = 0; // best y = 30000; // demerit // for instead of while since in original arduino code // if an early mask was "good enough" it wouldn't try for a better one // since they get more complex and take longer. for (k = 0; k < 8; k++) { applymask(k); // returns black-white imbalance x = badcheck(); if (x < y) { // current mask better than previous best? y = x; t = k; } if (t == 7) break; // don't increment i to a void redoing mask qrframe = strinbuf.slice(0); // reset for next pass } if (t != k) // redo best mask - none good enough, last wasn't t applymask(t); // add in final mask/ecclevel bytes y = fmtword[t + ((ecclevel - 1) << 3)]; // low byte for (k = 0; k < 8; k++ , y >>= 1) if (y & 1) { qrframe[(width - 1 - k) + width * 8] = 1; if (k < 6) qrframe[8 + width * k] = 1; else qrframe[8 + width * (k + 1)] = 1; } // high byte for (k = 0; k < 7; k++ , y >>= 1) if (y & 1) { qrframe[8 + width * (width - 7 + k)] = 1; if (k) qrframe[(6 - k) + width * 8] = 1; else qrframe[7 + width * 8] = 1; } return qrframe; } var _canvas = null; var api = { get ecclevel() { return ecclevel; }, set ecclevel(val) { ecclevel = val; }, get size() { return _size; }, set size(val) { _size = val }, get canvas() { return _canvas; }, set canvas(el) { _canvas = el; }, getFrame: function (string) { return genframe(string); }, //这里的utf16to8(str)是对Text中的字符串进行转码,让其支持中文 utf16to8: function (str) { var out, i, len, c; out = ""; len = str.length; for (i = 0; i < len; i++) { c = str.charCodeAt(i); if ((c >= 0x0001) && (c <= 0x007F)) { out += str.charAt(i); } else if (c > 0x07FF) { out += String.fromCharCode(0xE0 | ((c >> 12) & 0x0F)); out += String.fromCharCode(0x80 | ((c >> 6) & 0x3F)); out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F)); } else { out += String.fromCharCode(0xC0 | ((c >> 6) & 0x1F)); out += String.fromCharCode(0x80 | ((c >> 0) & 0x3F)); } } return out; }, /** * 新增$this参数,传入组件的this,兼容在组件中生成 * @param bg 目前只能设置颜色值 */ draw: function (str, ctx, startX, startY, cavW, cavH, bg, color, $this, ecc) { var that = this; ecclevel = ecc || ecclevel; if (!ctx) { console.warn('No canvas provided to draw QR code in!') return; } var size = Math.min(cavW, cavH); str = that.utf16to8(str);//增加中文显示 var frame = that.getFrame(str); var px = size / width; if (bg) { ctx.setFillStyle(bg) ctx.fillRect(startX, startY, cavW, cavW); } ctx.setFillStyle(color || 'black'); for (var i = 0; i < width; i++) { for (var j = 0; j < width; j++) { if (frame[j * width + i]) { ctx.fillRect(startX + px * i, startY + px * j, px, px); } } } } } module.exports = { api } // exports.draw = api; })(); ================================================ FILE: miniprogram/components/painter/lib/util.js ================================================ function isValidUrl(url) { return /(ht|f)tp(s?):\/\/([^ \\/]*\.)+[^ \\/]*(:[0-9]+)?\/?/.test(url); } /** * 深度对比两个对象是否一致 * from: https://github.com/epoberezkin/fast-deep-equal * @param {Object} a 对象a * @param {Object} b 对象b * @return {Boolean} 是否相同 */ /* eslint-disable */ function equal(a, b) { if (a === b) return true; if (a && b && typeof a == 'object' && typeof b == 'object') { var arrA = Array.isArray(a) , arrB = Array.isArray(b) , i , length , key; if (arrA && arrB) { length = a.length; if (length != b.length) return false; for (i = length; i-- !== 0;) if (!equal(a[i], b[i])) return false; return true; } if (arrA != arrB) return false; var dateA = a instanceof Date , dateB = b instanceof Date; if (dateA != dateB) return false; if (dateA && dateB) return a.getTime() == b.getTime(); var regexpA = a instanceof RegExp , regexpB = b instanceof RegExp; if (regexpA != regexpB) return false; if (regexpA && regexpB) return a.toString() == b.toString(); var keys = Object.keys(a); length = keys.length; if (length !== Object.keys(b).length) return false; for (i = length; i-- !== 0;) if (!Object.prototype.hasOwnProperty.call(b, keys[i])) return false; for (i = length; i-- !== 0;) { key = keys[i]; if (!equal(a[key], b[key])) return false; } return true; } return a!==a && b!==b; } module.exports = { isValidUrl, equal }; ================================================ FILE: miniprogram/components/painter/painter.js ================================================ import Pen from './lib/pen'; import Downloader from './lib/downloader'; const util = require('./lib/util'); const downloader = new Downloader(); // 最大尝试的绘制次数 const MAX_PAINT_COUNT = 5; Component({ canvasWidthInPx: 0, canvasHeightInPx: 0, paintCount: 0, /** * 组件的属性列表 */ properties: { customStyle: { type: String, }, palette: { type: Object, observer: function (newVal, oldVal) { if (this.isNeedRefresh(newVal, oldVal)) { this.paintCount = 0; this.startPaint(); } }, }, widthPixels: { type: Number, value: 0 }, // 启用脏检查,默认 false dirty: { type: Boolean, value: false, }, }, data: { picURL: '', showCanvas: true, painterStyle: '', }, methods: { /** * 判断一个 object 是否为 空 * @param {object} object */ isEmpty(object) { for (const i in object) { return false; } return true; }, isNeedRefresh(newVal, oldVal) { if (!newVal || this.isEmpty(newVal) || (this.data.dirty && util.equal(newVal, oldVal))) { return false; } return true; }, startPaint() { if (this.isEmpty(this.properties.palette)) { return; } if (!(getApp().systemInfo && getApp().systemInfo.screenWidth)) { try { getApp().systemInfo = wx.getSystemInfoSync(); } catch (e) { const error = `Painter get system info failed, ${JSON.stringify(e)}`; this.triggerEvent('imgErr', { error: error }); console.error(error); return; } } let screenK = getApp().systemInfo.screenWidth / 750; setStringPrototype(screenK, 1); this.downloadImages().then((palette) => { const { width, height } = palette; if (!width || !height) { console.error(`You should set width and height correctly for painter, width: ${width}, height: ${height}`); return; } this.canvasWidthInPx = width.toPx(); if (this.properties.widthPixels) { // 如果重新设置过像素宽度,则重新设置比例 setStringPrototype(screenK, this.properties.widthPixels / this.canvasWidthInPx) this.canvasWidthInPx = this.properties.widthPixels } this.canvasHeightInPx = height.toPx(); this.setData({ painterStyle: `width:${this.canvasWidthInPx}px;height:${this.canvasHeightInPx}px;`, }); const ctx = wx.createCanvasContext('k-canvas', this); const pen = new Pen(ctx, palette); pen.paint(() => { this.saveImgToLocal(); }); }); }, downloadImages() { return new Promise((resolve, reject) => { let preCount = 0; let completeCount = 0; const paletteCopy = JSON.parse(JSON.stringify(this.properties.palette)); if (paletteCopy.background) { preCount++; downloader.download(paletteCopy.background).then((path) => { paletteCopy.background = path; completeCount++; if (preCount === completeCount) { resolve(paletteCopy); } }, () => { completeCount++; if (preCount === completeCount) { resolve(paletteCopy); } }); } if (paletteCopy.views) { for (const view of paletteCopy.views) { if (view && view.type === 'image' && view.url) { preCount++; /* eslint-disable no-loop-func */ downloader.download(view.url).then((path) => { view.url = path; wx.getImageInfo({ src: view.url, success: (res) => { // 获得一下图片信息,供后续裁减使用 view.sWidth = res.width; view.sHeight = res.height; }, fail: (error) => { // 如果图片坏了,则直接置空,防止坑爹的 canvas 画崩溃了 view.url = ""; console.error(`getImageInfo ${view.url} failed, ${JSON.stringify(error)}`); }, complete: () => { completeCount++; if (preCount === completeCount) { resolve(paletteCopy); } }, }); }, () => { completeCount++; if (preCount === completeCount) { resolve(paletteCopy); } }); } } } if (preCount === 0) { resolve(paletteCopy); } }); }, saveImgToLocal() { setTimeout(() => { wx.canvasToTempFilePath({ canvasId: 'k-canvas', success: (res) => { this.getImageInfo(res.tempFilePath); }, fail: (error) => { console.error(`canvasToTempFilePath failed, ${JSON.stringify(error)}`); this.triggerEvent('imgErr', { error: error }); }, }, this); }, 300); }, getImageInfo(filePath) { wx.getImageInfo({ src: filePath, success: (infoRes) => { if (this.paintCount > MAX_PAINT_COUNT) { const error = `The result is always fault, even we tried ${MAX_PAINT_COUNT} times`; console.error(error); this.triggerEvent('imgErr', { error: error }); return; } // 比例相符时才证明绘制成功,否则进行强制重绘制 if (Math.abs((infoRes.width * this.canvasHeightInPx - this.canvasWidthInPx * infoRes.height) / (infoRes.height * this.canvasHeightInPx)) < 0.01) { this.triggerEvent('imgOK', { path: filePath }); } else { this.startPaint(); } this.paintCount++; }, fail: (error) => { console.error(`getImageInfo failed, ${JSON.stringify(error)}`); this.triggerEvent('imgErr', { error: error }); }, }); }, }, }); function setStringPrototype(screenK, scale) { /* eslint-disable no-extend-native */ /** * 是否支持负数 * @param {Boolean} minus 是否支持负数 */ String.prototype.toPx = function toPx(minus) { let reg; if (minus) { reg = /^-?[0-9]+([.]{1}[0-9]+){0,1}(rpx|px)$/g; } else { reg = /^[0-9]+([.]{1}[0-9]+){0,1}(rpx|px)$/g; } const results = reg.exec(this); if (!this || !results) { console.error(`The size: ${this} is illegal`); return 0; } const unit = results[2]; const value = parseFloat(this); let res = 0; if (unit === 'rpx') { res = Math.round(value * screenK * (scale || 1)); } else if (unit === 'px') { res = Math.round(value * (scale || 1)); } return res; }; } ================================================ FILE: miniprogram/components/painter/painter.json ================================================ { "component": true, "usingComponents": {} } ================================================ FILE: miniprogram/components/painter/painter.wxml ================================================ ================================================ FILE: miniprogram/components/pre-image/PreImage.js ================================================ // components/pre-image/PreImage.js Component({ externalClasses: 'extra-class', /** * 组件的属性列表 */ properties: { src: { type: String, value: '' }, mode: { type: String, value: 'scaleToFill' }, lazyLoad: { type: Boolean, value: false }, placeImg: { type: String, value: '/assets/images/img-loding.jpg' }, width: { type: String, value: '100%' }, height: { type: String, value: '0' } }, /** * 组件的初始数据 */ data: { loaded: false }, /** * 组件的方法列表 */ methods: { _onImageLoad: function(e) { const width = e.detail.width, height = e.detail.height; this.setData({ loaded: true }) this.triggerEvent('load', e.detail); }, _onImgeErr: function(e) { let msgs = e.detail.errMsg.split('('); this.setData({ error: "(" + msgs[msgs.length-1] }) this.triggerEvent('error', e.detail); } } }) ================================================ FILE: miniprogram/components/pre-image/PreImage.json ================================================ { "component": true, "usingComponents": {} } ================================================ FILE: miniprogram/components/pre-image/PreImage.wxml ================================================ {{error && error}} ================================================ FILE: miniprogram/components/pre-image/PreImage.wxss ================================================ /* components/pre-image/PreImage.wxss */ .comp-preImg { background-color: #eee; width: 100%; height: 100%; } .comp-preImg-pre { width: 0; height: 0; } .fade-in { animation: fadeIn 1s both; } .errors { width: 100%; height: 100%; background-color: rgba(0, 0, 0, .5); display: flex; justify-content: center; align-items: center; color: #F7AF41; } @keyframes fadeIn { 0% { opacity: 0 } 100% { opacity: 1 } } ================================================ FILE: miniprogram/components/rating/rating.js ================================================ // 组件-评分 Component({ behaviors: ['wx://form-field'], options: { addGlobalClass: true }, properties: { value: { type: Number, value: 10 }, max: { type: Number, value: 5 }, disabled: { type: Boolean, value: false } }, methods: { _handleTap (e) { if (this.data.disabled) return; const { max } = this.data; const { num } = e.currentTarget.dataset; const value = max / 5 * num; this.setData({ value }); this.triggerEvent('change', { value }, e); } } }) ================================================ FILE: miniprogram/components/rating/rating.json ================================================ { "component": true } ================================================ FILE: miniprogram/components/rating/rating.wxml ================================================ ================================================ FILE: miniprogram/components/rating/rating.wxss ================================================ /* components/rating/rating.wxss */ .com-rating { display: inline-block; letter-spacing: .3em; position: relative; } .com-rating .rating-icon, .com-rating .rating-on, .com-rating .rating-off { display: inline-block; } .com-rating .rating-icon:not(:last-child) { margin-right: .2em; } .com-rating .rating-on { color: #FFE200; position: absolute; overflow: hidden; padding: 0; margin: 0; } .com-rating .rating-off { color: #DBDBDB; padding: 0; margin: 0; } ================================================ FILE: miniprogram/components/tab-bar/index.js ================================================ Component({ properties: { selected: { type: Number, value: 0 }, placeholder: { type: Boolean, value: false }, list: { type: Array, value: [ { "pagePath": "/pages/tabs/discovery/discovery", "text": "发现", "iconPath": "/assets/images/tabbar/tabbar_discovery.png", "selectedIconPath": "/assets/images/tabbar/tabbar_discovery_selected.png" }, { "pagePath": "/pages/tabs/movies/movies", "text": "清单", "iconPath": "/assets/images/tabbar/tabbar_list.png", "selectedIconPath": "/assets/images/tabbar/tabbar_list_selected.png" }, { "pagePath": "/pages/tabs/index/index", "text": "我的", "iconPath": "/assets/images/tabbar/tabbar_mine.png", "selectedIconPath": "/assets/images/tabbar/tabbar_mine_selected.png" } ] } }, data: {}, methods: { handleSwitchTab (e) { /** @type {{ index: number }} */ const { index } = e.currentTarget.dataset; const { list } = this.data; this.setData({ selected: index }); this.triggerEvent('change', { index, list }); } } }); ================================================ FILE: miniprogram/components/tab-bar/index.json ================================================ { "component": true, "usingComponents": {} } ================================================ FILE: miniprogram/components/tab-bar/index.wxml ================================================ ================================================ FILE: miniprogram/components/tab-bar/index.wxss ================================================ .tab-bar-placeholder, .tab-bar { display: flex; height: 100rpx; border-radius: 60rpx 60rpx 0 0; padding-bottom: env(safe-area-inset-bottom); padding-bottom: constant(safe-area-inset-bottom); } .tab-bar { position: fixed; z-index: 9999; left: 0; right: 0; bottom: 0; background-color: #fff; box-shadow: 0 0 40rpx 0 rgba(0, 0, 0, 0.1); } @supports (padding-bottom: env(safe-area-inset-bottom)) { .tab-bar-placeholder, .tab-bar { padding-bottom: env(safe-area-inset-bottom); } } @supports (padding-bottom: constant(safe-area-inset-bottom)) { .tab-bar-placeholder, .tab-bar { padding-bottom: constant(safe-area-inset-bottom); } } .tab-bar-item { flex: 1; display: flex; flex-direction: column; justify-content: center; align-items: center; } .tab-bar-item__icon { display: block; width: 60rpx; height: 60rpx; } .tab-bar-item__icon:nth-child(2) { transform: rotate(180deg); } .tab-bar__icon-container { width: 120rpx; height: 60rpx; overflow: hidden; display: flex; justify-content: center; } .tab-bar__icon-wrapper { height: 120rpx; transform-origin: 50% 50%; transition: .3s ease-out; transition-property: none; } .tab-bar__icon-wrapper.selected { transition-property: transform; transform: rotate(180deg); } ================================================ FILE: miniprogram/components/towxml/audio-player/Audio.js ================================================ const fillIn = val => `${val < 10 ? '0' : ''}${val}`, formatTime = _time => { let time = Math.round(_time); let second = Math.round(time % 60), minute = Math.floor(time / 60 % 60), hour = Math.floor(time / 60 / 60); return `${fillIn(hour)}:${fillIn(minute)}:${fillIn(second)}`; }; class Audio{ constructor(obj){ const _ts = this, option = _ts.option = obj.attr; _ts.loop = option.loop === 'true', _ts.autoplay = option.autoplay === 'true'; _ts.create(); _ts.index = 0; } create(){ const _ts = this, option = _ts.option; let audio = _ts.audio = wx.createInnerAudioContext(); audio.src = option.src; // 说明可以播放了 audio.onCanplay(function(){ if(_ts.autoplay && !_ts.index){ _ts.play(); }; if(!_ts.autoplay && !_ts.index){ _ts.eventCanplay(); }; }); // 更新时间 audio.onTimeUpdate(function(){ //_ts.status = 'update'; _ts.duration = audio.duration; _ts.currentTime = audio.currentTime; // 定义播放结束 if(_ts.duration - _ts.currentTime < 0.5){ _ts.index++; if(_ts.loop){ audio.stop(); }else{ _ts.stop(); }; audio.seek(0); }; _ts.eventTimeUpdate(formatTime(_ts.duration),formatTime(_ts.currentTime)); }); // audio.onSeeked(function(){ if(_ts.loop){ _ts.play(); }; }); } // 播放 play(){ const _ts = this; _ts.status = 'play'; _ts.audio.play(); _ts.eventPlay(); } // 暂停 pause(){ const _ts = this; _ts.status = 'pause'; _ts.audio.pause(); _ts.eventPause(); } // 停止 stop(){ const _ts = this; _ts.status = 'stop'; _ts.audio.stop(); _ts.eventStop(); } // 销毁 destroy(){ const _ts = this; _ts.stop(); _ts.audio.destroy(); } eventCanplay(){} eventTimeUpdate(){} eventEnded(){} eventError(){} eventPause(){} eventPlay(){} eventSeeked(){} eventSeeking(){} eventStop(){} eventWaiting(){} }; module.exports = Audio; ================================================ FILE: miniprogram/components/towxml/audio-player/audio-player.js ================================================ const Audio = require('./Audio'); Component({ options: { styleIsolation: 'shared' }, properties: { data: { type: Object, value: {} } }, lifetimes:{ // 页面生命周期 attached:function(){ const _ts = this, audio = _ts.audio = new Audio(this.data.data); audio.eventPlay = function(){ _ts.setData({tips:{state:'h2w__audio--play',text:'Playing'}}); }; audio.eventCanplay = function(){ _ts.setData({tips:{state:'h2w__audio--readyed',text:'Readyed'}}); }; audio.eventTimeUpdate = function(duration,currentTime){ _ts.setData({time:{currentTime:currentTime,duration:duration,schedule:Math.round(_ts.audio.currentTime) / Math.round(_ts.audio.duration) * 100 + '%'}}); }; audio.eventPause = function(){ _ts.setData({tips:{state:'h2w__audio--pause',text:'Pause'}}); }; audio.eventStop = function(){ _ts.setData({tips:{state:'h2w__audio--end',text:'End'}}); }; // // 更新播放状态 // _ts.audio.onTimeUpdate = function(duration,currentTime){ // _ts.setData({ // playerData:{ // state:'h2w__audio--play', // tips:'Playing', // currentTime:currentTime, // duration:duration, // schedule:_ts.audio.currentTime / _ts.audio.duration * 100 + '%' // } // }); // }; // _ts.audio.onPause = function(){ // _ts.setData({playerData:{state:'h2w__audio--pause',tips:'Pause'}}); // }; // _ts.audio.onCanplay = function(){ // _ts.setData({playerData:{state:'h2w__audio--readyed',tips:'Readyed'}}); // }; // _ts.audio.onError = function(){ // _ts.setData({playerData:{state:'h2w__audio--error',tips:'Error'}}); // }; // _ts.audio.onEnded = ()=>{ // _ts.setData({playerData:{state:'h2w__audio--end',tips:'End'}}); // }; }, moved:function(){ _ts.audio.destroy(); }, detached:()=>{ }, }, data: { tips:{ state:'', text:'--' }, time: { currentTime:'00:00:00', duration:'00:00:00', schedule:'0%' } }, methods: { playAndPause: function () { const _ts = this, audio = _ts.audio; // console.log(audio); audio.isTouch = true; if(audio.status === 'update' || audio.status === 'play'){ // console.log('pause'); audio.pause(); }else{ // console.log('play'); audio.play(); }; } } }) ================================================ FILE: miniprogram/components/towxml/audio-player/audio-player.json ================================================ { "component": true, "usingComponents": { } } ================================================ FILE: miniprogram/components/towxml/audio-player/audio-player.wxml ================================================ {{tips.text || 'Error'}} {{data.attr.name}} {{data.attr.author}} {{time.currentTime || '00:00:00'}} / {{time.duration || '00:00:00'}} ================================================ FILE: miniprogram/components/towxml/audio-player/audio-player.wxss ================================================ /*音频播放器样式*/ .h2w__audio { height: 136rpx; margin:16rpx 0; background: #f1f1f1; position: relative; } .h2w__audio--error .h2w__audioIcon, .h2w__audio--loading .h2w__audioIcon { display:none; } .h2w__audio--readyed .h2w__audioLoading, .h2w__audio--end .h2w__audioLoading, .h2w__audio--play .h2w__audioIcon, .h2w__audio--pause .h2w__audioLoading, .h2w__audio--play .h2w__audioLoading { display: none; } .h2w__audio--play .h2w__audioCover image { opacity: 1; } .h2w__audio--readyed .h2w__audioTips, .h2w__audio--end .h2w__audioTips, .h2w__audio--stop .h2w__audioTips, .h2w__audio--pause .h2w__audioTips, .h2w__audio--play .h2w__audioTips { opacity:0.4; } .h2w__audio--error { background:red; } /* .h2w__audio--end .h2w__audio__icon {width:20rpx; height:20rpx; background:white; border:0; left:24rpx; top:24rpx; border-radius:2rpx;} */ .h2w__audioCover { width: 136rpx; height: 136rpx; background: black; float: left; position: relative; } .h2w__audioCover image { width: 100%; height: 100%; opacity: 0.6; margin:0; transition: all 0.5s cubic-bezier(0.075, 0.82, 0.165, 1); } .h2w__audioCover .h2w__audioLoading { width:80rpx; height:80rpx; position:absolute; left:50%; top:50%; margin:-40rpx 0 0 -40rpx; z-index:1; opacity:1; } .h2w__audioInfo { padding-left: 20rpx; padding-top: 16rpx; position: absolute; left: 136rpx; right: 0; } .h2w__audioSchedule { position: absolute; left: 0; top: 0; background: rgba(0, 255, 0, 0.1); height: 136rpx; width: 0; } .h2w__audioTips { position:absolute; right:0; top:0; height: 32rpx; line-height: 32rpx; padding:10rpx 20rpx; font-size:20rpx; } .h2w__audio--error .h2w__audioTips { color:red; } .h2w__audioTitle { display: block; font-size: 24rpx; height: 40rpx; line-height: 40rpx; font-weight: bold; } .h2w__audioAuthor { display: block; font-size: 20rpx; height: 32rpx; line-height: 32rpx; } .h2w__audioTime { display: block; font-size: 20rpx; height: 32rpx; line-height: 32rpx; } .h2w__audioIcon { width: 0; height: 0; position: absolute; left: 60rpx; top: 48rpx; border-width: 20rpx 0 20rpx 20rpx; border-style: solid; border-color: transparent transparent transparent #fff; z-index: 1; } /* 深色主题 */ .h2w-dark .h2w__audio { background: #1f1f1f; } .h2w-dark .h2w__audio--error { background:rgba(255,0,0,0.1); } .h2w-dark .h2w__audioCover { background: black; } .h2w-dark .h2w__audioSchedule { background: rgba(0, 255, 0, 0.2); } .h2w-dark .h2w__audioIcon { border-color: transparent transparent transparent #fff; } /* 浅色主题 */ .h2w-light .h2w__audio { background: #f1f1f1; } .h2w-light .h2w__audio--error { background:rgba(255,0,0,0.1); } .h2w-light .h2w__audioCover { background: black; } .h2w-light .h2w__audioSchedule { background: rgba(0, 255, 0, 0.1); } .h2w-light .h2w__audioIcon { border-color: transparent transparent transparent #fff; } ================================================ FILE: miniprogram/components/towxml/config.js ================================================ module.exports = { // LaTex公式、yuml解析服务架设参见 https://github.com/sbfkcel/markdown-server // 数学公式解析API // latex:{ // api:'http://towxml.vvadd.com/?tex' // }, // yuml图解析APPI // yuml:{ // api:'http://towxml.vvadd.com/?yuml' // }, // markdown解析配置,保留需要的选项即可 markdown:[ 'sub', // 下标支持 'sup', // 上标支持 'ins', // 文本删除线支持 'mark', // 文本高亮支持 'emoji', // emoji表情支持 'todo' // todo支持 ], // 代码高亮配置,保留需要的选项即可(尽量越少越好,不要随意调整顺序。部分高亮有顺序依赖) highlight:[ 'c-like', 'c', 'bash', 'css', 'dart', 'go', 'java', 'javascript', 'json', 'less', 'scss', 'shell', 'xml', 'htmlbars', 'nginx', 'php', 'python', 'python-repl', 'typescript', // 'csharp', // 'http', // 'swift', // 'yaml', // 'markdown', // 'powershell', // 'ruby', // 'makefile', // 'lua', // 'stylus', // 'basic', // '1c', // 'abnf', // 'accesslog', // 'actionscript', // 'ada', // 'angelscript', // 'apache', // 'applescript', // 'arcade', // 'cpp', // 'arduino', // 'armasm', // 'asciidoc', // 'aspectj', // 'autohotkey', // 'autoit', // 'avrasm', // 'awk', // 'axapta', // 'bnf', // 'brainfuck', // 'cal', // 'capnproto', // 'ceylon', // 'clean', // 'clojure-repl', // 'clojure', // 'cmake', // 'coffeescript', // 'coq', // 'cos', // 'crmsh', // 'crystal', // 'csp', // 'd', // 'delphi', // 'diff', // 'django', // 'dns', // 'dockerfile', // 'dos', // 'dsconfig', // 'dts', // 'dust', // 'ebnf', // 'elixir', // 'elm', // 'erb', // 'erlang-repl', // 'erlang', // 'excel', // 'fix', // 'flix', // 'fortran', // 'fsharp', // 'gams', // 'gauss', // 'gcode', // 'gherkin', // 'glsl', // 'gml', // 'golo', // 'gradle', // 'groovy', // 'haml', // 'handlebars', // 'haskell', // 'haxe', // 'hsp', // 'hy', // 'inform7', // 'ini', // 'irpf90', // 'isbl', // 'jboss-cli', // 'julia-repl', // 'julia', // 'kotlin', // 'lasso', // 'latex', // 'ldif', // 'leaf', // 'lisp', // 'livecodeserver', // 'livescript', // 'llvm', // 'lsl', // 'mathematica', // 'matlab', // 'maxima', // 'mel', // 'mercury', // 'mipsasm', // 'mizar', // 'mojolicious', // 'monkey', // 'moonscript', // 'n1ql', // 'nim', // 'nix', // 'nsis', // 'objectivec', // 'ocaml', // 'openscad', // 'oxygene', // 'parser3', // 'perl', // 'pf', // 'pgsql', // 'php-template', // 'plaintext', // 'pony', // 'processing', // 'profile', // 'prolog', // 'properties', // 'protobuf', // 'puppet', // 'purebasic', // 'q', // 'qml', // 'r', // 'reasonml', // 'rib', // 'roboconf', // 'routeros', // 'rsl', // 'ruleslanguage', // 'rust', // 'sas', // 'scala', // 'scheme', // 'scilab', // 'smali', // 'smalltalk', // 'sml', // 'sqf', // 'sql', // 'stan', // 'stata', // 'step21', // 'subunit', // 'taggerscript', // 'tap', // 'tcl', // 'thrift', // 'tp', // 'twig', // 'vala', // 'vbnet', // 'vbscript-html', // 'vbscript', // 'verilog', // 'vhdl', // 'vim', // 'x86asm', // 'xl', // 'xquery', // 'zephir' ], // wxml原生标签,该系列标签将不会被转换 wxml:[ 'view', 'video', 'text', 'image', 'navigator', 'swiper', 'swiper-item', 'block', 'form', 'input', 'textarea', 'button', 'checkbox-group', 'checkbox', 'radio-group', 'radio', // 可以解析的标签(html或markdown中会很少使用) // 'canvas', // 'map', // 'slider', // 'scroll-view', // 'movable-area', // 'movable-view', // 'progress', // 'label', // 'switch', // 'picker', // 'picker-view', // 'switch', // 'contact-button' ], // 自定义组件 components:[ 'audio-player', // 音频组件,建议保留,由于小程序原生audio存在诸多问题,towxml解决了原生音频播放器的相关问题 // 'echarts', // echarts图表支持 // 'latex', // 数学公式支持 'table', // 表格支持 // 'todogroup', // todo支持 // 'yuml', // yuml图表支持 'img' // 图片解析组件 ], // 保留原本的元素属性(建议不要变动) attrs:[ 'class', 'data', 'id', 'style' ], // 事件绑定方式(catch或bind),catch 会阻止事件向上冒泡。更多请参考:https://developers.weixin.qq.com/miniprogram/dev/framework/view/wxml/event.html bindType:'catch', // 需要激活的事件 events:[ // 'touchstart', // 'touchmove', // 'touchcancel', // 'touchend', 'tap', // 用于元素的点击事件 'change', // 用于todoList的change事件 ], // 图片倍数 dpr:1, // 代码块显示行号 showLineNumber: false } ================================================ FILE: miniprogram/components/towxml/decode.js ================================================ const config = require('./config'); Component({ options: { styleIsolation: 'apply-shared' }, properties: { nodes: { type: Object, value: {} } }, lifetimes: { attached: function () { const _ts = this; config.events.forEach(item => { _ts['_' + item] = function (...arg) { if (global._events && typeof global._events[item] === 'function') { global._events[item](...arg); } }; }); } } }) ================================================ FILE: miniprogram/components/towxml/decode.json ================================================ { "component": true, "usingComponents": { "decode": "./decode", "audio-player": "./audio-player/audio-player", "table": "./table/table", "img": "./img/img" } } ================================================ FILE: miniprogram/components/towxml/decode.wxml ================================================ {{item.text}}
================================================ FILE: miniprogram/components/towxml/decode.wxss ================================================ ================================================ FILE: miniprogram/components/towxml/img/img.js ================================================ const config = require('../config'); Component({ options: { styleIsolation: 'shared' }, properties: { data: { type: Object, value: {} } }, data: { attr:{ src:'', class:'', style:'' }, size:{ w:0, h:0 }, styleObj:{} }, lifetimes:{ attached:function(){ const _ts = this; let dataAttr = this.data.data.attr; // 将图片大小处理到对象中 if(dataAttr.width){ _ts.data.size.w = +dataAttr.width / config.dpr; }; if(dataAttr.height){ _ts.data.size.h = +dataAttr.height / config.dpr; }; // 将样式合并到样式对象中 if(dataAttr.style){ let re = /;\s{0,}/ig; dataAttr.style = dataAttr.style.replace(re,';'); dataAttr.style.split(';').forEach(item => { let itemArr = item.split(':'); if(/^(width|height)$/i.test(itemArr[0])){ let num = parseInt(itemArr[1]) || 0, key = ''; // itemArr[1] = num / config.dpr + itemArr[1].replace(num,''); switch (itemArr[0].toLocaleLowerCase()) { case 'width': key = 'w'; break; case 'height': key = 'h'; break; }; _ts.data.size[key] = num / config.dpr; }else{ _ts.data.styleObj[itemArr[0]] = itemArr[1]; }; }); }; // 设置公式图片 _ts.setData({ attr:{ src:dataAttr.src, class:dataAttr.class, style:_ts.setStyle(_ts.data.styleObj) }, size:_ts.data.size }); } }, methods: { // 设置图片样式 setStyle:function(o){ let str = ``; for(let key in o){ str += `${key}:${o[key]};`; }; return str; }, // 图片加载完成设置图片大小 load:function(e){ const _ts = this; if(!_ts.data.size.w || !_ts.data.size.h){ _ts.setData({ size:{ w:e.detail.width / config.dpr, h:e.detail.height / config.dpr } }); }; } } }) ================================================ FILE: miniprogram/components/towxml/img/img.json ================================================ { "component": true } ================================================ FILE: miniprogram/components/towxml/img/img.wxml ================================================ ================================================ FILE: miniprogram/components/towxml/img/img.wxss ================================================ ================================================ FILE: miniprogram/components/towxml/index.js ================================================ const md = require('./parse/markdown/index'), parse = require('./parse/index') module.exports = (str,type,option)=>{ option = option || {}; let result; switch (type) { case 'markdown': result = parse(md(str),option); break; case 'html': result = parse(str,option); break; default: throw new Error('Invalid type, only markdown and html are supported'); break; }; return result; }; ================================================ FILE: miniprogram/components/towxml/parse/highlight/highlight.js ================================================ "use strict";function deepFreeze(e){Object.freeze(e);var n="function"==typeof e;return Object.getOwnPropertyNames(e).forEach(function(t){!e.hasOwnProperty(t)||null===e[t]||"object"!=typeof e[t]&&"function"!=typeof e[t]||n&&("caller"===t||"callee"===t||"arguments"===t)||Object.isFrozen(e[t])||deepFreeze(e[t])}),e}function escapeHTML(e){return e.replace(/&/g,"&").replace(//g,">")}function inherit(e){var n,t={},r=Array.prototype.slice.call(arguments,1);for(n in e)t[n]=e[n];return r.forEach(function(e){for(n in e)t[n]=e[n]}),t}function tag(e){return e.nodeName.toLowerCase()}function nodeStream(e){var n=[];return function e(t,r){for(var a=t.firstChild;a;a=a.nextSibling)3===a.nodeType?r+=a.nodeValue.length:1===a.nodeType&&(n.push({event:"start",offset:r,node:a}),r=e(a,r),tag(a).match(/br|hr|img|input/)||n.push({event:"stop",offset:r,node:a}));return r}(e,0),n}function mergeStreams(e,n,t){var r=0,a="",i=[];function s(){return e.length&&n.length?e[0].offset!==n[0].offset?e[0].offset"}function l(e){a+=""}function c(e){("start"===e.event?o:l)(e.node)}for(;e.length||n.length;){var u=s();if(a+=escapeHTML(t.substring(r,u[0].offset)),r=u[0].offset,u===e){i.reverse().forEach(l);do{c(u.splice(0,1)[0]),u=s()}while(u===e&&u.length&&u[0].offset===r);i.reverse().forEach(o)}else"start"===u[0].event?i.push(u[0].node):i.pop(),c(u.splice(0,1)[0])}return a+escapeHTML(t.substr(r))}var utils=Object.freeze({__proto__:null,escapeHTML:escapeHTML,inherit:inherit,nodeStream:nodeStream,mergeStreams:mergeStreams});const SPAN_CLOSE="",emitsWrappingTags=e=>!!e.kind;class HTMLRenderer{constructor(e,n){this.buffer="",this.classPrefix=n.classPrefix,e.walk(this)}addText(e){this.buffer+=escapeHTML(e)}openNode(e){if(!emitsWrappingTags(e))return;let n=e.kind;e.sublanguage||(n=`${this.classPrefix}${n}`),this.span(n)}closeNode(e){emitsWrappingTags(e)&&(this.buffer+=SPAN_CLOSE)}span(e){this.buffer+=``}value(){return this.buffer}}class TokenTree{constructor(){this.rootNode={children:[]},this.stack=[this.rootNode]}get top(){return this.stack[this.stack.length-1]}get root(){return this.rootNode}add(e){this.top.children.push(e)}openNode(e){let n={kind:e,children:[]};this.add(n),this.stack.push(n)}closeNode(){if(this.stack.length>1)return this.stack.pop()}closeAllNodes(){for(;this.closeNode(););}toJSON(){return JSON.stringify(this.rootNode,null,4)}walk(e){return this.constructor._walk(e,this.rootNode)}static _walk(e,n){return"string"==typeof n?e.addText(n):n.children&&(e.openNode(n),n.children.forEach(n=>this._walk(e,n)),e.closeNode(n)),e}static _collapse(e){e.children&&(e.children.every(e=>"string"==typeof e)?(e.text=e.children.join(""),delete e.children):e.children.forEach(e=>{"string"!=typeof e&&TokenTree._collapse(e)}))}}class TokenTreeEmitter extends TokenTree{constructor(e){super(),this.options=e}addKeyword(e,n){""!==e&&(this.openNode(n),this.addText(e),this.closeNode())}addText(e){""!==e&&this.add(e)}addSublanguage(e,n){let t=e.root;t.kind=n,t.sublanguage=!0,this.add(t)}toHTML(){return new HTMLRenderer(this,this.options).value()}finalize(){}}function escape(e){return new RegExp(e.replace(/[-\/\\^$*+?.()|[\]{}]/g,"\\$&"),"m")}function source(e){return e&&e.source||e}function countMatchGroups(e){return new RegExp(e.toString()+"|").exec("").length-1}function startsWith(e,n){var t=e&&e.exec(n);return t&&0===t.index}function join(e,n){for(var t=/\[(?:[^\\\]]|\\.)*\]|\(\??|\\([1-9][0-9]*)|\\./,r=0,a="",i=0;i0&&(a+=n),a+="(";o.length>0;){var l=t.exec(o);if(null==l){a+=o;break}a+=o.substring(0,l.index),o=o.substring(l.index+l[0].length),"\\"==l[0][0]&&l[1]?a+="\\"+String(Number(l[1])+s):(a+=l[0],"("==l[0]&&r++)}a+=")"}return a}const IDENT_RE="[a-zA-Z]\\w*",UNDERSCORE_IDENT_RE="[a-zA-Z_]\\w*",NUMBER_RE="\\b\\d+(\\.\\d+)?",C_NUMBER_RE="(-?)(\\b0[xX][a-fA-F0-9]+|(\\b\\d+(\\.\\d*)?|\\.\\d+)([eE][-+]?\\d+)?)",BINARY_NUMBER_RE="\\b(0b[01]+)",RE_STARTERS_RE="!|!=|!==|%|%=|&|&&|&=|\\*|\\*=|\\+|\\+=|,|-|-=|/=|/|:|;|<<|<<=|<=|<|===|==|=|>>>=|>>=|>=|>>>|>>|>|\\?|\\[|\\{|\\(|\\^|\\^=|\\||\\|=|\\|\\||~",BACKSLASH_ESCAPE={begin:"\\\\[\\s\\S]",relevance:0},APOS_STRING_MODE={className:"string",begin:"'",end:"'",illegal:"\\n",contains:[BACKSLASH_ESCAPE]},QUOTE_STRING_MODE={className:"string",begin:'"',end:'"',illegal:"\\n",contains:[BACKSLASH_ESCAPE]},PHRASAL_WORDS_MODE={begin:/\b(a|an|the|are|I'm|isn't|don't|doesn't|won't|but|just|should|pretty|simply|enough|gonna|going|wtf|so|such|will|you|your|they|like|more)\b/},COMMENT=function(e,n,t){var r=inherit({className:"comment",begin:e,end:n,contains:[]},t||{});return r.contains.push(PHRASAL_WORDS_MODE),r.contains.push({className:"doctag",begin:"(?:TODO|FIXME|NOTE|BUG|XXX):",relevance:0}),r},C_LINE_COMMENT_MODE=COMMENT("//","$"),C_BLOCK_COMMENT_MODE=COMMENT("/\\*","\\*/"),HASH_COMMENT_MODE=COMMENT("#","$"),NUMBER_MODE={className:"number",begin:NUMBER_RE,relevance:0},C_NUMBER_MODE={className:"number",begin:C_NUMBER_RE,relevance:0},BINARY_NUMBER_MODE={className:"number",begin:"\\b(0b[01]+)",relevance:0},CSS_NUMBER_MODE={className:"number",begin:NUMBER_RE+"(%|em|ex|ch|rem|vw|vh|vmin|vmax|cm|mm|in|pt|pc|px|deg|grad|rad|turn|s|ms|Hz|kHz|dpi|dpcm|dppx)?",relevance:0},REGEXP_MODE={begin:/(?=\/[^\/\n]*\/)/,contains:[{className:"regexp",begin:/\//,end:/\/[gimuy]*/,illegal:/\n/,contains:[BACKSLASH_ESCAPE,{begin:/\[/,end:/\]/,relevance:0,contains:[BACKSLASH_ESCAPE]}]}]},TITLE_MODE={className:"title",begin:IDENT_RE,relevance:0},UNDERSCORE_TITLE_MODE={className:"title",begin:"[a-zA-Z_]\\w*",relevance:0},METHOD_GUARD={begin:"\\.\\s*[a-zA-Z_]\\w*",relevance:0};var MODES=Object.freeze({__proto__:null,IDENT_RE:IDENT_RE,UNDERSCORE_IDENT_RE:"[a-zA-Z_]\\w*",NUMBER_RE:NUMBER_RE,C_NUMBER_RE:C_NUMBER_RE,BINARY_NUMBER_RE:"\\b(0b[01]+)",RE_STARTERS_RE:RE_STARTERS_RE,BACKSLASH_ESCAPE:BACKSLASH_ESCAPE,APOS_STRING_MODE:APOS_STRING_MODE,QUOTE_STRING_MODE:QUOTE_STRING_MODE,PHRASAL_WORDS_MODE:PHRASAL_WORDS_MODE,COMMENT:COMMENT,C_LINE_COMMENT_MODE:C_LINE_COMMENT_MODE,C_BLOCK_COMMENT_MODE:C_BLOCK_COMMENT_MODE,HASH_COMMENT_MODE:HASH_COMMENT_MODE,NUMBER_MODE:NUMBER_MODE,C_NUMBER_MODE:C_NUMBER_MODE,BINARY_NUMBER_MODE:BINARY_NUMBER_MODE,CSS_NUMBER_MODE:CSS_NUMBER_MODE,REGEXP_MODE:REGEXP_MODE,TITLE_MODE:TITLE_MODE,UNDERSCORE_TITLE_MODE:UNDERSCORE_TITLE_MODE,METHOD_GUARD:METHOD_GUARD}),COMMON_KEYWORDS="of and for in not or if then".split(" ");function compileLanguage(e){function n(n,t){return new RegExp(source(n),"m"+(e.case_insensitive?"i":"")+(t?"g":""))}class t{constructor(){this.matchIndexes={},this.regexes=[],this.matchAt=1,this.position=0}addRule(e,n){n.position=this.position++,this.matchIndexes[this.matchAt]=n,this.regexes.push([n,e]),this.matchAt+=countMatchGroups(e)+1}compile(){0===this.regexes.length&&(this.exec=(()=>null));let e=this.regexes.map(e=>e[1]);this.matcherRe=n(join(e,"|"),!0),this.lastIndex=0}exec(e){this.matcherRe.lastIndex=this.lastIndex;let n=this.matcherRe.exec(e);if(!n)return null;let t=n.findIndex((e,n)=>n>0&&void 0!=e),r=this.matchIndexes[t];return Object.assign(n,r)}}class r{constructor(){this.rules=[],this.multiRegexes=[],this.count=0,this.lastIndex=0,this.regexIndex=0}getMatcher(e){if(this.multiRegexes[e])return this.multiRegexes[e];let n=new t;return this.rules.slice(e).forEach(([e,t])=>n.addRule(e,t)),n.compile(),this.multiRegexes[e]=n,n}considerAll(){this.regexIndex=0}addRule(e,n){this.rules.push([e,n]),"begin"===n.type&&this.count++}exec(e){let n=this.getMatcher(this.regexIndex);n.lastIndex=this.lastIndex;let t=n.exec(e);return t&&(this.regexIndex+=t.position+1,this.regexIndex===this.count&&(this.regexIndex=0)),t}}function a(e){let n=e.input[e.index-1],t=e.input[e.index+e[0].length];if("."===n||"."===t)return{ignoreMatch:!0}}if(e.contains&&e.contains.includes("self"))throw new Error("ERR: contains `self` is not supported at the top-level of a language. See documentation.");!function t(i,s){i.compiled||(i.compiled=!0,i.__onBegin=null,i.keywords=i.keywords||i.beginKeywords,i.keywords&&(i.keywords=compileKeywords(i.keywords,e.case_insensitive)),i.lexemesRe=n(i.lexemes||/\w+/,!0),s&&(i.beginKeywords&&(i.begin="\\b("+i.beginKeywords.split(" ").join("|")+")(?=\\b|\\s)",i.__onBegin=a),i.begin||(i.begin=/\B|\b/),i.beginRe=n(i.begin),i.endSameAsBegin&&(i.end=i.begin),i.end||i.endsWithParent||(i.end=/\B|\b/),i.end&&(i.endRe=n(i.end)),i.terminator_end=source(i.end)||"",i.endsWithParent&&s.terminator_end&&(i.terminator_end+=(i.end?"|":"")+s.terminator_end)),i.illegal&&(i.illegalRe=n(i.illegal)),null==i.relevance&&(i.relevance=1),i.contains||(i.contains=[]),i.contains=[].concat(...i.contains.map(function(e){return expand_or_clone_mode("self"===e?i:e)})),i.contains.forEach(function(e){t(e,i)}),i.starts&&t(i.starts,s),i.matcher=function(e){let n=new r;return e.contains.forEach(e=>n.addRule(e.begin,{rule:e,type:"begin"})),e.terminator_end&&n.addRule(e.terminator_end,{type:"end"}),e.illegal&&n.addRule(e.illegal,{type:"illegal"}),n}(i))}(e)}function dependencyOnParent(e){return!!e&&(e.endsWithParent||dependencyOnParent(e.starts))}function expand_or_clone_mode(e){return e.variants&&!e.cached_variants&&(e.cached_variants=e.variants.map(function(n){return inherit(e,{variants:null},n)})),e.cached_variants?e.cached_variants:dependencyOnParent(e)?inherit(e,{starts:e.starts?inherit(e.starts):null}):Object.isFrozen(e)?inherit(e):e}function compileKeywords(e,n){var t={};return"string"==typeof e?r("keyword",e):Object.keys(e).forEach(function(n){r(n,e[n])}),t;function r(e,r){n&&(r=r.toLowerCase()),r.split(" ").forEach(function(n){var r=n.split("|");t[r[0]]=[e,scoreForKeyword(r[0],r[1])]})}}function scoreForKeyword(e,n){return n?Number(n):commonKeyword(e)?0:1}function commonKeyword(e){return COMMON_KEYWORDS.includes(e.toLowerCase())}var version="10.0.0-beta.0";const escape$1=escapeHTML,inherit$1=inherit,{nodeStream:nodeStream$1,mergeStreams:mergeStreams$1}=utils,HLJS=function(e){var n=[],t={},r={},a=[],i=!0,s=/((^(<[^>]+>|\t|)+|(?:\n)))/gm,o="Could not find the language '{}', did you forget to load/include a language module?",l={noHighlightRe:/^(no-?highlight)$/i,languageDetectRe:/\blang(?:uage)?-([\w-]+)\b/i,classPrefix:"hljs-",tabReplace:null,useBR:!1,languages:void 0,__emitter:TokenTreeEmitter};function c(e){return l.noHighlightRe.test(e)}function u(e,n,t,r){var a={code:n,language:e};R("before:highlight",a);var i=a.result?a.result:d(a.language,a.code,t,r);return i.code=a.code,R("after:highlight",i),i}function d(e,n,r,a){var s=n;function c(e,n){var t=R.case_insensitive?n[0].toLowerCase():n[0];return e.keywords.hasOwnProperty(t)&&e.keywords[t]}function u(){null!=b.subLanguage?function(){if(""!==S){var e="string"==typeof b.subLanguage;if(!e||t[b.subLanguage]){var n=e?d(b.subLanguage,S,!0,v[b.subLanguage]):g(S,b.subLanguage.length?b.subLanguage:void 0);b.relevance>0&&(T+=n.relevance),e&&(v[b.subLanguage]=n.top),N.addSublanguage(n.emitter,n.language)}else N.addText(S)}}():function(){var e,n,t,r;if(b.keywords){for(n=0,b.lexemesRe.lastIndex=0,t=b.lexemesRe.exec(S),r="";t;){r+=S.substring(n,t.index);var a=null;(e=c(b,t))?(N.addText(r),r="",T+=e[1],a=e[0],N.addKeyword(t[0],a)):r+=t[0],n=b.lexemesRe.lastIndex,t=b.lexemesRe.exec(S)}r+=S.substr(n),N.addText(r)}else N.addText(S)}(),S=""}function h(e){e.className&&N.openNode(e.className),b=Object.create(e,{parent:{value:b}})}function f(e){var n=e[0],t=e.rule;if(t.__onBegin){if((t.__onBegin(e)||{}).ignoreMatch)return function(e){return 0===b.matcher.regexIndex?(S+=e[0],1):(w=!0,0)}(n)}return t&&t.endSameAsBegin&&(t.endRe=escape(n)),t.skip?S+=n:(t.excludeBegin&&(S+=n),u(),t.returnBegin||t.excludeBegin||(S=n)),h(t),t.returnBegin?0:n.length}function E(e){var n=e[0],t=s.substr(e.index),r=function e(n,t){if(startsWith(n.endRe,t)){for(;n.endsParent&&n.parent;)n=n.parent;return n}if(n.endsWithParent)return e(n.parent,t)}(b,t);if(r){var a=b;a.skip?S+=n:(a.returnEnd||a.excludeEnd||(S+=n),u(),a.excludeEnd&&(S=n));do{b.className&&N.closeNode(),b.skip||b.subLanguage||(T+=b.relevance),b=b.parent}while(b!==r.parent);return r.starts&&(r.endSameAsBegin&&(r.starts.endRe=r.endRe),h(r.starts)),a.returnEnd?0:n.length}}var _={};function m(n,t){var a,o=t&&t[0];if(S+=n,null==o)return u(),0;if("begin"==_.type&&"end"==t.type&&_.index==t.index&&""===o){if(S+=s.slice(t.index,t.index+1),!i)throw(a=new Error("0 width match regex")).languageName=e,a.badRule=_.rule,a;return 1}if(_=t,"begin"===t.type)return f(t);if("illegal"===t.type&&!r)throw(a=new Error('Illegal lexeme "'+o+'" for mode "'+(b.className||"")+'"')).mode=b,a;if("end"===t.type){var l=E(t);if(void 0!=l)return l}return S+=o,o.length}var R=p(e);if(!R)throw console.error(o.replace("{}",e)),new Error('Unknown language: "'+e+'"');compileLanguage(R);var M,b=a||R,v={},N=new l.__emitter(l);!function(){for(var e=[],n=b;n!==R;n=n.parent)n.className&&e.unshift(n.className);e.forEach(e=>N.openNode(e))}();var O,x,S="",T=0,D=0;try{var w=!1;for(b.matcher.considerAll();w?w=!1:(b.matcher.lastIndex=D,b.matcher.considerAll()),O=b.matcher.exec(s);){x=m(s.substring(D,O.index),O),D=O.index+x}return m(s.substr(D)),N.closeAllNodes(),N.finalize(),M=N.toHTML(),{relevance:T,value:M,language:e,illegal:!1,emitter:N,top:b}}catch(n){if(n.message&&n.message.includes("Illegal"))return{illegal:!0,illegalBy:{msg:n.message,context:s.slice(D-100,D+100),mode:n.mode},sofar:M,relevance:0,value:escape$1(s),emitter:N};if(i)return{relevance:0,value:escape$1(s),emitter:N,language:e,top:b,errorRaised:n};throw n}}function g(e,n){n=n||l.languages||Object.keys(t);var r={relevance:0,emitter:new l.__emitter(l),value:escape$1(e)},a=r;return n.filter(p).filter(m).forEach(function(n){var t=d(n,e,!1);t.language=n,t.relevance>a.relevance&&(a=t),t.relevance>r.relevance&&(a=r,r=t)}),a.language&&(r.second_best=a),r}function h(e){return l.tabReplace||l.useBR?e.replace(s,function(e,n){return l.useBR&&"\n"===e?"
":l.tabReplace?n.replace(/\t/g,l.tabReplace):""}):e}function f(e){var n,t,a,i,s,d=function(e){var n,t=e.className+" ";if(t+=e.parentNode?e.parentNode.className:"",n=l.languageDetectRe.exec(t)){var r=p(n[1]);return r||(console.warn(o.replace("{}",n[1])),console.warn("Falling back to no-highlight mode for this block.",e)),r?n[1]:"no-highlight"}return t.split(/\s+/).find(e=>c(e)||p(e))}(e);c(d)||(R("before:highlightBlock",{block:e,language:d}),l.useBR?(n=document.createElement("div")).innerHTML=e.innerHTML.replace(/\n/g,"").replace(//g,"\n"):n=e,s=n.textContent,a=d?u(d,s,!0):g(s),(t=nodeStream$1(n)).length&&((i=document.createElement("div")).innerHTML=a.value,a.value=mergeStreams$1(t,nodeStream$1(i),s)),a.value=h(a.value),R("after:highlightBlock",{block:e,result:a}),e.innerHTML=a.value,e.className=function(e,n,t){var a=n?r[n]:t,i=[e.trim()];return e.match(/\bhljs\b/)||i.push("hljs"),e.includes(a)||i.push(a),i.join(" ").trim()}(e.className,d,a.language),e.result={language:a.language,re:a.relevance},a.second_best&&(e.second_best={language:a.second_best.language,re:a.second_best.relevance}))}function E(){if(!E.called){E.called=!0;var e=document.querySelectorAll("pre code");n.forEach.call(e,f)}}var _={disableAutodetect:!0};function p(e){return e=(e||"").toLowerCase(),t[e]||t[r[e]]}function m(e){var n=p(e);return n&&!n.disableAutodetect}function R(e,n){var t=e;a.forEach(function(e){e[t]&&e[t](n)})}Object.assign(e,{highlight:u,highlightAuto:g,fixMarkup:h,highlightBlock:f,configure:function(e){l=inherit$1(l,e)},initHighlighting:E,initHighlightingOnLoad:function(){window.addEventListener("DOMContentLoaded",E,!1)},registerLanguage:function(n,a){var s;try{s=a(e)}catch(e){if(console.error("Language definition for '{}' could not be registered.".replace("{}",n)),!i)throw e;console.error(e),s=_}s.name||(s.name=n),t[n]=s,s.rawDefinition=a.bind(null,e),s.aliases&&s.aliases.forEach(function(e){r[e]=n})},listLanguages:function(){return Object.keys(t)},getLanguage:p,requireLanguage:function(e){var n=p(e);if(n)return n;throw new Error("The '{}' language is required, but not loaded.".replace("{}",e))},autoDetection:m,inherit:inherit$1,addPlugin:function(e,n){a.push(e)}}),e.debugMode=function(){i=!1},e.safeMode=function(){i=!0},e.versionString=version;for(const e in MODES)"object"==typeof MODES[e]&&deepFreeze(MODES[e]);return Object.assign(e,MODES),e};var highlight=HLJS({});module.exports=highlight; ================================================ FILE: miniprogram/components/towxml/parse/highlight/index.js ================================================ import config from '../../config'; import hljs from './highlight'; const languages ={ 'c-like': require('./languages/c-like'), c: require('./languages/c'), bash: require('./languages/bash'), css: require('./languages/css'), dart: require('./languages/dart'), go: require('./languages/go'), java: require('./languages/go'), javascript: require('./languages/javascript'), json: require('./languages/json'), less: require('./languages/less'), scss: require('./languages/scss'), shell: require('./languages/shell'), xml: require('./languages/xml'), htmlbars: require('./languages/htmlbars'), 'nginx': require('./languages/nginx'), 'php': require('./languages/php'), 'python': require('./languages/python'), 'python-repl': require('./languages/python-repl'), 'typescript': require('./languages/typescript'), }; for (const lang of config.highlight) { hljs.registerLanguage(lang, languages[lang].default); } module.exports = hljs; ================================================ FILE: miniprogram/components/towxml/parse/highlight/languages/bash.js ================================================ /* Language: Bash Author: vah Contributrors: Benjamin Pannell Website: https://www.gnu.org/software/bash/ Category: common */ export default function(hljs) { const VAR = {}; const BRACED_VAR = { begin: /\$\{/, end:/\}/, contains: [ { begin: /:-/, contains: [VAR] } // default values ] }; Object.assign(VAR,{ className: 'variable', variants: [ {begin: /\$[\w\d#@][\w\d_]*/}, BRACED_VAR ] }); const SUBST = { className: 'subst', begin: /\$\(/, end: /\)/, contains: [hljs.BACKSLASH_ESCAPE] }; const QUOTE_STRING = { className: 'string', begin: /"/, end: /"/, contains: [ hljs.BACKSLASH_ESCAPE, VAR, SUBST ] }; SUBST.contains.push(QUOTE_STRING); const ESCAPED_QUOTE = { className: '', begin: /\\"/ }; const APOS_STRING = { className: 'string', begin: /'/, end: /'/ }; const ARITHMETIC = { begin: /\$\(\(/, end: /\)\)/, contains: [ { begin: /\d+#[0-9a-f]+/, className: "number" }, hljs.NUMBER_MODE, VAR ] }; const SHEBANG = { className: 'meta', begin: /^#![^\n]+sh\s*$/, relevance: 10 }; const FUNCTION = { className: 'function', begin: /\w[\w\d_]*\s*\(\s*\)\s*\{/, returnBegin: true, contains: [hljs.inherit(hljs.TITLE_MODE, {begin: /\w[\w\d_]*/})], relevance: 0 }; return { name: 'Bash', aliases: ['sh', 'zsh'], lexemes: /\b-?[a-z\._]+\b/, keywords: { keyword: 'if then else elif fi for while in do done case esac function', literal: 'true false', built_in: // Shell built-ins // http://www.gnu.org/software/bash/manual/html_node/Shell-Builtin-Commands.html 'break cd continue eval exec exit export getopts hash pwd readonly return shift test times ' + 'trap umask unset ' + // Bash built-ins 'alias bind builtin caller command declare echo enable help let local logout mapfile printf ' + 'read readarray source type typeset ulimit unalias ' + // Shell modifiers 'set shopt ' + // Zsh built-ins 'autoload bg bindkey bye cap chdir clone comparguments compcall compctl compdescribe compfiles ' + 'compgroups compquote comptags comptry compvalues dirs disable disown echotc echoti emulate ' + 'fc fg float functions getcap getln history integer jobs kill limit log noglob popd print ' + 'pushd pushln rehash sched setcap setopt stat suspend ttyctl unfunction unhash unlimit ' + 'unsetopt vared wait whence where which zcompile zformat zftp zle zmodload zparseopts zprof ' + 'zpty zregexparse zsocket zstyle ztcp', _: '-ne -eq -lt -gt -f -d -e -s -l -a' // relevance booster }, contains: [ SHEBANG, FUNCTION, ARITHMETIC, hljs.HASH_COMMENT_MODE, QUOTE_STRING, ESCAPED_QUOTE, APOS_STRING, VAR ] }; } ================================================ FILE: miniprogram/components/towxml/parse/highlight/languages/c-like.js ================================================ /* Language: C-like foundation grammar for C/C++ grammars Author: Ivan Sagalaev Contributors: Evgeny Stepanischev , Zaven Muradyan , Roel Deckers , Sam Wu , Jordi Petit , Pieter Vantorre , Google Inc. (David Benjamin) Category: common, system */ /* In the future the intention is to split out the C/C++ grammars distinctly since they are separate languages. They will likely share a common foundation though, and this file sets the groundwork for that - so that we get the breaking change in v10 and don't have to change the requirements again later. See: https://github.com/highlightjs/highlight.js/issues/2146 */ export default function(hljs) { function optional(s) { return '(?:' + s + ')?'; } var DECLTYPE_AUTO_RE = 'decltype\\(auto\\)' var NAMESPACE_RE = '[a-zA-Z_]\\w*::' var TEMPLATE_ARGUMENT_RE = '<.*?>'; var FUNCTION_TYPE_RE = '(' + DECLTYPE_AUTO_RE + '|' + optional(NAMESPACE_RE) +'[a-zA-Z_]\\w*' + optional(TEMPLATE_ARGUMENT_RE) + ')'; var CPP_PRIMITIVE_TYPES = { className: 'keyword', begin: '\\b[a-z\\d_]*_t\\b' }; // https://en.cppreference.com/w/cpp/language/escape // \\ \x \xFF \u2837 \u00323747 \374 var CHARACTER_ESCAPES = '\\\\(x[0-9A-Fa-f]{2}|u[0-9A-Fa-f]{4,8}|[0-7]{3}|\\S)' var STRINGS = { className: 'string', variants: [ { begin: '(u8?|U|L)?"', end: '"', illegal: '\\n', contains: [hljs.BACKSLASH_ESCAPE] }, { begin: '(u8?|U|L)?\'(' + CHARACTER_ESCAPES + "|.)", end: '\'', illegal: '.' }, { begin: /(?:u8?|U|L)?R"([^()\\ ]{0,16})\((?:.|\n)*?\)\1"/ } ] }; var NUMBERS = { className: 'number', variants: [ { begin: '\\b(0b[01\']+)' }, { begin: '(-?)\\b([\\d\']+(\\.[\\d\']*)?|\\.[\\d\']+)(u|U|l|L|ul|UL|f|F|b|B)' }, { begin: '(-?)(\\b0[xX][a-fA-F0-9\']+|(\\b[\\d\']+(\\.[\\d\']*)?|\\.[\\d\']+)([eE][-+]?[\\d\']+)?)' } ], relevance: 0 }; var PREPROCESSOR = { className: 'meta', begin: /#\s*[a-z]+\b/, end: /$/, keywords: { 'meta-keyword': 'if else elif endif define undef warning error line ' + 'pragma _Pragma ifdef ifndef include' }, contains: [ { begin: /\\\n/, relevance: 0 }, hljs.inherit(STRINGS, {className: 'meta-string'}), { className: 'meta-string', begin: /<.*?>/, end: /$/, illegal: '\\n', }, hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE ] }; var TITLE_MODE = { className: 'title', begin: optional(NAMESPACE_RE) + hljs.IDENT_RE, relevance: 0 }; var FUNCTION_TITLE = optional(NAMESPACE_RE) + hljs.IDENT_RE + '\\s*\\('; var CPP_KEYWORDS = { keyword: 'int float while private char char8_t char16_t char32_t catch import module export virtual operator sizeof ' + 'dynamic_cast|10 typedef const_cast|10 const for static_cast|10 union namespace ' + 'unsigned long volatile static protected bool template mutable if public friend ' + 'do goto auto void enum else break extern using asm case typeid wchar_t ' + 'short reinterpret_cast|10 default double register explicit signed typename try this ' + 'switch continue inline delete alignas alignof constexpr consteval constinit decltype ' + 'concept co_await co_return co_yield requires ' + 'noexcept static_assert thread_local restrict final override ' + 'atomic_bool atomic_char atomic_schar ' + 'atomic_uchar atomic_short atomic_ushort atomic_int atomic_uint atomic_long atomic_ulong atomic_llong ' + 'atomic_ullong new throw return ' + 'and and_eq bitand bitor compl not not_eq or or_eq xor xor_eq', built_in: 'std string wstring cin cout cerr clog stdin stdout stderr stringstream istringstream ostringstream ' + 'auto_ptr deque list queue stack vector map set bitset multiset multimap unordered_set ' + 'unordered_map unordered_multiset unordered_multimap array shared_ptr abort terminate abs acos ' + 'asin atan2 atan calloc ceil cosh cos exit exp fabs floor fmod fprintf fputs free frexp ' + 'fscanf future isalnum isalpha iscntrl isdigit isgraph islower isprint ispunct isspace isupper ' + 'isxdigit tolower toupper labs ldexp log10 log malloc realloc memchr memcmp memcpy memset modf pow ' + 'printf putchar puts scanf sinh sin snprintf sprintf sqrt sscanf strcat strchr strcmp ' + 'strcpy strcspn strlen strncat strncmp strncpy strpbrk strrchr strspn strstr tanh tan ' + 'vfprintf vprintf vsprintf endl initializer_list unique_ptr _Bool complex _Complex imaginary _Imaginary', literal: 'true false nullptr NULL' }; var EXPRESSION_CONTAINS = [ CPP_PRIMITIVE_TYPES, hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, NUMBERS, STRINGS ]; var EXPRESSION_CONTEXT = { // This mode covers expression context where we can't expect a function // definition and shouldn't highlight anything that looks like one: // `return some()`, `else if()`, `(x*sum(1, 2))` variants: [ {begin: /=/, end: /;/}, {begin: /\(/, end: /\)/}, {beginKeywords: 'new throw return else', end: /;/} ], keywords: CPP_KEYWORDS, contains: EXPRESSION_CONTAINS.concat([ { begin: /\(/, end: /\)/, keywords: CPP_KEYWORDS, contains: EXPRESSION_CONTAINS.concat(['self']), relevance: 0 } ]), relevance: 0 }; var FUNCTION_DECLARATION = { className: 'function', begin: '(' + FUNCTION_TYPE_RE + '[\\*&\\s]+)+' + FUNCTION_TITLE, returnBegin: true, end: /[{;=]/, excludeEnd: true, keywords: CPP_KEYWORDS, illegal: /[^\w\s\*&:<>]/, contains: [ { // to prevent it from being confused as the function title begin: DECLTYPE_AUTO_RE, keywords: CPP_KEYWORDS, relevance: 0, }, { begin: FUNCTION_TITLE, returnBegin: true, contains: [TITLE_MODE], relevance: 0 }, { className: 'params', begin: /\(/, end: /\)/, keywords: CPP_KEYWORDS, relevance: 0, contains: [ hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, STRINGS, NUMBERS, CPP_PRIMITIVE_TYPES, // Count matching parentheses. { begin: /\(/, end: /\)/, keywords: CPP_KEYWORDS, relevance: 0, contains: [ 'self', hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, STRINGS, NUMBERS, CPP_PRIMITIVE_TYPES ] } ] }, CPP_PRIMITIVE_TYPES, hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, PREPROCESSOR ] }; return { aliases: ['c', 'cc', 'h', 'c++', 'h++', 'hpp', 'hh', 'hxx', 'cxx'], keywords: CPP_KEYWORDS, // the base c-like language will NEVER be auto-detected, rather the // derivitives: c, c++, arduino turn auto-detect back on for themselves disableAutodetect: true, illegal: '', keywords: CPP_KEYWORDS, contains: ['self', CPP_PRIMITIVE_TYPES] }, { begin: hljs.IDENT_RE + '::', keywords: CPP_KEYWORDS }, { className: 'class', beginKeywords: 'class struct', end: /[{;:]/, contains: [ {begin: //, contains: ['self']}, // skip generic stuff hljs.TITLE_MODE ] } ]), exports: { preprocessor: PREPROCESSOR, strings: STRINGS, keywords: CPP_KEYWORDS } }; } ================================================ FILE: miniprogram/components/towxml/parse/highlight/languages/c.js ================================================ /* Language: C Category: common, system Website: https://en.wikipedia.org/wiki/C_(programming_language) Requires: c-like.js */ export default function(hljs) { var lang = hljs.getLanguage('c-like').rawDefinition(); // Until C is actually different than C++ there is no reason to auto-detect C // as it's own language since it would just fail auto-detect testing or // simply match with C++. // // See further comments in c-like.js. // lang.disableAutodetect = false; lang.name = 'C'; lang.aliases = ['c', 'h']; return lang; } ================================================ FILE: miniprogram/components/towxml/parse/highlight/languages/css.js ================================================ /* Language: CSS Category: common, css Website: https://developer.mozilla.org/en-US/docs/Web/CSS */ export default function(hljs) { var FUNCTION_LIKE = { begin: /[\w-]+\(/, returnBegin: true, contains: [ { className: 'built_in', begin: /[\w-]+/ }, { begin: /\(/, end: /\)/, contains: [ hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, hljs.CSS_NUMBER_MODE, ] } ] } var ATTRIBUTE = { className: 'attribute', begin: /\S/, end: ':', excludeEnd: true, starts: { endsWithParent: true, excludeEnd: true, contains: [ FUNCTION_LIKE, hljs.CSS_NUMBER_MODE, hljs.QUOTE_STRING_MODE, hljs.APOS_STRING_MODE, hljs.C_BLOCK_COMMENT_MODE, { className: 'number', begin: '#[0-9A-Fa-f]+' }, { className: 'meta', begin: '!important' } ] } } var AT_IDENTIFIER = '@[a-z-]+' // @font-face var AT_MODIFIERS = "and or not only" var MEDIA_TYPES = "all print screen speech" var AT_PROPERTY_RE = /@\-?\w[\w]*(\-\w+)*/ // @-webkit-keyframes var IDENT_RE = '[a-zA-Z-][a-zA-Z0-9_-]*'; var RULE = { begin: /(?:[A-Z\_\.\-]+|--[a-zA-Z0-9_-]+)\s*:/, returnBegin: true, end: ';', endsWithParent: true, contains: [ ATTRIBUTE ] }; return { name: 'CSS', case_insensitive: true, illegal: /[=\/|'\$]/, contains: [ hljs.C_BLOCK_COMMENT_MODE, { className: 'selector-id', begin: /#[A-Za-z0-9_-]+/ }, { className: 'selector-class', begin: /\.[A-Za-z0-9_-]+/ }, { className: 'selector-attr', begin: /\[/, end: /\]/, illegal: '$', contains: [ hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, ] }, { className: 'selector-pseudo', begin: /:(:)?[a-zA-Z0-9\_\-\+\(\)"'.]+/ }, // matching these here allows us to treat them more like regular CSS // rules so everything between the {} gets regular rule highlighting, // which is what we want for page and font-face { begin: '@(page|font-face)', lexemes: AT_IDENTIFIER, keywords: '@page @font-face' }, { begin: '@', end: '[{;]', // at_rule eating first "{" is a good thing // because it doesn’t let it to be parsed as // a rule set but instead drops parser into // the default mode which is how it should be. illegal: /:/, // break on Less variables @var: ... returnBegin: true, contains: [ { className: 'keyword', begin: AT_PROPERTY_RE }, { begin: /\s/, endsWithParent: true, excludeEnd: true, relevance: 0, keywords: AT_MODIFIERS, contains: [ { begin: /[a-z-]+:/, className:"attribute" }, hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, hljs.CSS_NUMBER_MODE ] } ] }, { className: 'selector-tag', begin: IDENT_RE, relevance: 0 }, { begin: '{', end: '}', illegal: /\S/, contains: [ hljs.C_BLOCK_COMMENT_MODE, RULE, ] } ] }; } ================================================ FILE: miniprogram/components/towxml/parse/highlight/languages/dart.js ================================================ /* Language: Dart Requires: markdown.js Author: Maxim Dikun Description: Dart a modern, object-oriented language developed by Google. For more information see https://www.dartlang.org/ Website: https://dart.dev Category: scripting */ export default function(hljs) { var SUBST = { className: 'subst', variants: [{ begin: '\\$[A-Za-z0-9_]+' }], }; var BRACED_SUBST = { className: 'subst', variants: [{ begin: '\\${', end: '}' }, ], keywords: 'true false null this is new super', }; var STRING = { className: 'string', variants: [{ begin: 'r\'\'\'', end: '\'\'\'' }, { begin: 'r"""', end: '"""' }, { begin: 'r\'', end: '\'', illegal: '\\n' }, { begin: 'r"', end: '"', illegal: '\\n' }, { begin: '\'\'\'', end: '\'\'\'', contains: [hljs.BACKSLASH_ESCAPE, SUBST, BRACED_SUBST] }, { begin: '"""', end: '"""', contains: [hljs.BACKSLASH_ESCAPE, SUBST, BRACED_SUBST] }, { begin: '\'', end: '\'', illegal: '\\n', contains: [hljs.BACKSLASH_ESCAPE, SUBST, BRACED_SUBST] }, { begin: '"', end: '"', illegal: '\\n', contains: [hljs.BACKSLASH_ESCAPE, SUBST, BRACED_SUBST] } ] }; BRACED_SUBST.contains = [ hljs.C_NUMBER_MODE, STRING ]; var KEYWORDS = { keyword: 'abstract as assert async await break case catch class const continue covariant default deferred do ' + 'dynamic else enum export extends extension external factory false final finally for Function get hide if ' + 'implements import in inferface is library mixin new null on operator part rethrow return set show static ' + 'super switch sync this throw true try typedef var void while with yield', built_in: // dart:core 'Comparable DateTime Duration Function Iterable Iterator List Map Match Null Object Pattern RegExp Set ' + 'Stopwatch String StringBuffer StringSink Symbol Type Uri bool double dynamic int num print ' + // dart:html 'Element ElementList document querySelector querySelectorAll window' }; return { name: 'Dart', keywords: KEYWORDS, contains: [ STRING, hljs.COMMENT( '/\\*\\*', '\\*/', { subLanguage: 'markdown', relevance:0 } ), hljs.COMMENT( '///+\\s*', '$', { contains: [{ subLanguage: 'markdown', begin: '.', end: '$', relevance:0 }] } ), hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, { className: 'class', beginKeywords: 'class interface', end: '{', excludeEnd: true, contains: [{ beginKeywords: 'extends implements' }, hljs.UNDERSCORE_TITLE_MODE ] }, hljs.C_NUMBER_MODE, { className: 'meta', begin: '@[A-Za-z]+' }, { begin: '=>' // No markup, just a relevance booster } ] } } ================================================ FILE: miniprogram/components/towxml/parse/highlight/languages/go.js ================================================ /* Language: Go Author: Stephan Kountso aka StepLg Contributors: Evgeny Stepanischev Description: Google go language (golang). For info about language Website: http://golang.org/ Category: common, system */ export default function(hljs) { var GO_KEYWORDS = { keyword: 'break default func interface select case map struct chan else goto package switch ' + 'const fallthrough if range type continue for import return var go defer ' + 'bool byte complex64 complex128 float32 float64 int8 int16 int32 int64 string uint8 ' + 'uint16 uint32 uint64 int uint uintptr rune', literal: 'true false iota nil', built_in: 'append cap close complex copy imag len make new panic print println real recover delete' }; return { name: 'Go', aliases: ['golang'], keywords: GO_KEYWORDS, illegal: ' Description: Matcher for HTMLBars Website: https://github.com/tildeio/htmlbars Category: template */ export default function(hljs) { var BUILT_INS = 'action collection component concat debugger each each-in else get hash if input link-to loc log mut outlet partial query-params render textarea unbound unless with yield view'; var ATTR_ASSIGNMENT = { illegal: /\}\}/, begin: /[a-zA-Z0-9_]+=/, returnBegin: true, relevance: 0, contains: [ { className: 'attr', begin: /[a-zA-Z0-9_]+/ } ] }; var SUB_EXPR = { illegal: /\}\}/, begin: /\)/, end: /\)/, contains: [ { begin: /[a-zA-Z\.\-]+/, keywords: {built_in: BUILT_INS}, starts: { endsWithParent: true, relevance: 0, contains: [ hljs.QUOTE_STRING_MODE, ] } } ] }; var TAG_INNARDS = { endsWithParent: true, relevance: 0, keywords: {keyword: 'as', built_in: BUILT_INS}, contains: [ hljs.QUOTE_STRING_MODE, ATTR_ASSIGNMENT, hljs.NUMBER_MODE ] }; return { name: 'HTMLBars', case_insensitive: true, subLanguage: 'xml', contains: [ hljs.COMMENT('{{!(--)?', '(--)?}}'), { className: 'template-tag', begin: /\{\{[#\/]/, end: /\}\}/, contains: [ { className: 'name', begin: /[a-zA-Z\.\-]+/, keywords: {'builtin-name': BUILT_INS}, starts: TAG_INNARDS } ] }, { className: 'template-variable', begin: /\{\{[a-zA-Z][a-zA-Z\-]+/, end: /\}\}/, keywords: {keyword: 'as', built_in: BUILT_INS}, contains: [ hljs.QUOTE_STRING_MODE ] } ] }; } ================================================ FILE: miniprogram/components/towxml/parse/highlight/languages/index.js ================================================ export * as cLike from './c-like'; export * as c from './c'; export * as bash from './bash'; export * as css from './css'; export * as dart from './dart'; export * as go from './go'; export * as java from './go'; export * as javascript from './javascript'; export * as json from './json'; export * as less from './less'; export * as scss from './scss'; export * as shell from './shell'; export * as xml from './xml'; export * as htmlbars from './htmlbars'; export * as nginx from './nginx'; export * as php from './php'; export * as python from './python'; export * as pythonRepl from './python-repl'; export * as typescript from './typescript'; ================================================ FILE: miniprogram/components/towxml/parse/highlight/languages/java.js ================================================ /* Language: Java Author: Vsevolod Solovyov Category: common, enterprise Website: https://www.java.com/ */ export default function(hljs) { var JAVA_IDENT_RE = '[\u00C0-\u02B8a-zA-Z_$][\u00C0-\u02B8a-zA-Z_$0-9]*'; var GENERIC_IDENT_RE = JAVA_IDENT_RE + '(<' + JAVA_IDENT_RE + '(\\s*,\\s*' + JAVA_IDENT_RE + ')*>)?'; var KEYWORDS = 'false synchronized int abstract float private char boolean var static null if const ' + 'for true while long strictfp finally protected import native final void ' + 'enum else break transient catch instanceof byte super volatile case assert short ' + 'package default double public try this switch continue throws protected public private ' + 'module requires exports do'; var ANNOTATION = { className: 'meta', begin: '@' + JAVA_IDENT_RE, contains:[ { begin: /\(/, end: /\)/, contains: ["self"] // allow nested () inside our annotation }, ] } // https://docs.oracle.com/javase/7/docs/technotes/guides/language/underscores-literals.html var JAVA_NUMBER_RE = '\\b' + '(' + '0[bB]([01]+[01_]+[01]+|[01]+)' + // 0b... '|' + '0[xX]([a-fA-F0-9]+[a-fA-F0-9_]+[a-fA-F0-9]+|[a-fA-F0-9]+)' + // 0x... '|' + '(' + '([\\d]+[\\d_]+[\\d]+|[\\d]+)(\\.([\\d]+[\\d_]+[\\d]+|[\\d]+))?' + '|' + '\\.([\\d]+[\\d_]+[\\d]+|[\\d]+)' + ')' + '([eE][-+]?\\d+)?' + // octal, decimal, float ')' + '[lLfF]?'; var JAVA_NUMBER_MODE = { className: 'number', begin: JAVA_NUMBER_RE, relevance: 0 }; return { name: 'Java', aliases: ['jsp'], keywords: KEYWORDS, illegal: /<\/|#/, contains: [ hljs.COMMENT( '/\\*\\*', '\\*/', { relevance : 0, contains : [ { // eat up @'s in emails to prevent them to be recognized as doctags begin: /\w+@/, relevance: 0 }, { className : 'doctag', begin : '@[A-Za-z]+' } ] } ), hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, { className: 'class', beginKeywords: 'class interface', end: /[{;=]/, excludeEnd: true, keywords: 'class interface', illegal: /[:"\[\]]/, contains: [ {beginKeywords: 'extends implements'}, hljs.UNDERSCORE_TITLE_MODE ] }, { // Expression keywords prevent 'keyword Name(...)' from being // recognized as a function definition beginKeywords: 'new throw return else', relevance: 0 }, { className: 'function', begin: '(' + GENERIC_IDENT_RE + '\\s+)+' + hljs.UNDERSCORE_IDENT_RE + '\\s*\\(', returnBegin: true, end: /[{;=]/, excludeEnd: true, keywords: KEYWORDS, contains: [ { begin: hljs.UNDERSCORE_IDENT_RE + '\\s*\\(', returnBegin: true, relevance: 0, contains: [hljs.UNDERSCORE_TITLE_MODE] }, { className: 'params', begin: /\(/, end: /\)/, keywords: KEYWORDS, relevance: 0, contains: [ ANNOTATION, hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, hljs.C_NUMBER_MODE, hljs.C_BLOCK_COMMENT_MODE ] }, hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE ] }, JAVA_NUMBER_MODE, ANNOTATION ] }; } ================================================ FILE: miniprogram/components/towxml/parse/highlight/languages/javascript.js ================================================ /* Language: JavaScript Description: JavaScript (JS) is a lightweight, interpreted, or just-in-time compiled programming language with first-class functions. Category: common, scripting Website: https://developer.mozilla.org/en-US/docs/Web/JavaScript */ export default function(hljs) { var FRAGMENT = { begin: '<>', end: '' }; var XML_TAG = { begin: /<[A-Za-z0-9\\._:-]+/, end: /\/[A-Za-z0-9\\._:-]+>|\/>/ }; var IDENT_RE = '[A-Za-z$_][0-9A-Za-z$_]*'; var KEYWORDS = { keyword: 'in of if for while finally var new function do return void else break catch ' + 'instanceof with throw case default try this switch continue typeof delete ' + 'let yield const export super debugger as async await static ' + // ECMAScript 6 modules import 'import from as' , literal: 'true false null undefined NaN Infinity', built_in: 'eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent ' + 'encodeURI encodeURIComponent escape unescape Object Function Boolean Error ' + 'EvalError InternalError RangeError ReferenceError StopIteration SyntaxError ' + 'TypeError URIError Number Math Date String RegExp Array Float32Array ' + 'Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array ' + 'Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require ' + 'module console window document Symbol Set Map WeakSet WeakMap Proxy Reflect ' + 'Promise' }; var NUMBER = { className: 'number', variants: [ { begin: '\\b(0[bB][01]+)n?' }, { begin: '\\b(0[oO][0-7]+)n?' }, { begin: hljs.C_NUMBER_RE + 'n?' } ], relevance: 0 }; var SUBST = { className: 'subst', begin: '\\$\\{', end: '\\}', keywords: KEYWORDS, contains: [] // defined later }; var HTML_TEMPLATE = { begin: 'html`', end: '', starts: { end: '`', returnEnd: false, contains: [ hljs.BACKSLASH_ESCAPE, SUBST ], subLanguage: 'xml', } }; var CSS_TEMPLATE = { begin: 'css`', end: '', starts: { end: '`', returnEnd: false, contains: [ hljs.BACKSLASH_ESCAPE, SUBST ], subLanguage: 'css', } }; var TEMPLATE_STRING = { className: 'string', begin: '`', end: '`', contains: [ hljs.BACKSLASH_ESCAPE, SUBST ] }; SUBST.contains = [ hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, HTML_TEMPLATE, CSS_TEMPLATE, TEMPLATE_STRING, NUMBER, hljs.REGEXP_MODE ]; var PARAMS_CONTAINS = SUBST.contains.concat([ hljs.C_BLOCK_COMMENT_MODE, hljs.C_LINE_COMMENT_MODE ]); var PARAMS = { className: 'params', begin: /\(/, end: /\)/, excludeBegin: true, excludeEnd: true, contains: PARAMS_CONTAINS }; return { name: 'JavaScript', aliases: ['js', 'jsx', 'mjs', 'cjs'], keywords: KEYWORDS, contains: [ { className: 'meta', relevance: 10, begin: /^\s*['"]use (strict|asm)['"]/ }, { className: 'meta', begin: /^#!/, end: /$/ }, hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, HTML_TEMPLATE, CSS_TEMPLATE, TEMPLATE_STRING, hljs.C_LINE_COMMENT_MODE, hljs.COMMENT( '/\\*\\*', '\\*/', { relevance : 0, contains : [ { className : 'doctag', begin : '@[A-Za-z]+', contains : [ { className: 'type', begin: '\\{', end: '\\}', relevance: 0 }, { className: 'variable', begin: IDENT_RE + '(?=\\s*(-)|$)', endsParent: true, relevance: 0 }, // eat spaces (not newlines) so we can find // types or variables { begin: /(?=[^\n])\s/, relevance: 0 }, ] } ] } ), hljs.C_BLOCK_COMMENT_MODE, NUMBER, { // object attr container begin: /[{,\n]\s*/, relevance: 0, contains: [ { begin: IDENT_RE + '\\s*:', returnBegin: true, relevance: 0, contains: [{className: 'attr', begin: IDENT_RE, relevance: 0}] } ] }, { // "value" container begin: '(' + hljs.RE_STARTERS_RE + '|\\b(case|return|throw)\\b)\\s*', keywords: 'return throw case', contains: [ hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, hljs.REGEXP_MODE, { className: 'function', begin: '(\\(.*?\\)|' + IDENT_RE + ')\\s*=>', returnBegin: true, end: '\\s*=>', contains: [ { className: 'params', variants: [ { begin: IDENT_RE }, { begin: /\(\s*\)/, }, { begin: /\(/, end: /\)/, excludeBegin: true, excludeEnd: true, keywords: KEYWORDS, contains: PARAMS_CONTAINS } ] } ] }, { // could be a comma delimited list of params to a function call begin: /,/, relevance: 0, }, { className: '', begin: /\s/, end: /\s*/, skip: true, }, { // JSX variants: [ { begin: FRAGMENT.begin, end: FRAGMENT.end }, { begin: XML_TAG.begin, end: XML_TAG.end } ], subLanguage: 'xml', contains: [ { begin: XML_TAG.begin, end: XML_TAG.end, skip: true, contains: ['self'] } ] }, ], relevance: 0 }, { className: 'function', beginKeywords: 'function', end: /\{/, excludeEnd: true, contains: [ hljs.inherit(hljs.TITLE_MODE, {begin: IDENT_RE}), PARAMS ], illegal: /\[|%/ }, { begin: /\$[(.]/ // relevance booster for a pattern common to JS libs: `$(something)` and `$.something` }, hljs.METHOD_GUARD, { // ES6 class className: 'class', beginKeywords: 'class', end: /[{;=]/, excludeEnd: true, illegal: /[:"\[\]]/, contains: [ {beginKeywords: 'extends'}, hljs.UNDERSCORE_TITLE_MODE ] }, { beginKeywords: 'constructor', end: /\{/, excludeEnd: true }, { begin:'(get|set)\\s*(?=' + IDENT_RE+ '\\()', end: /{/, keywords: "get set", contains: [ hljs.inherit(hljs.TITLE_MODE, {begin: IDENT_RE}), { begin: /\(\)/ }, // eat to avoid empty params PARAMS ] } ], illegal: /#(?!!)/ }; } ================================================ FILE: miniprogram/components/towxml/parse/highlight/languages/json.js ================================================ /* Language: JSON Description: JSON (JavaScript Object Notation) is a lightweight data-interchange format. Author: Ivan Sagalaev Website: http://www.json.org Category: common, protocols */ export default function(hljs) { var LITERALS = {literal: 'true false null'}; var ALLOWED_COMMENTS = [ hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE ] var TYPES = [ hljs.QUOTE_STRING_MODE, hljs.C_NUMBER_MODE ]; var VALUE_CONTAINER = { end: ',', endsWithParent: true, excludeEnd: true, contains: TYPES, keywords: LITERALS }; var OBJECT = { begin: '{', end: '}', contains: [ { className: 'attr', begin: /"/, end: /"/, contains: [hljs.BACKSLASH_ESCAPE], illegal: '\\n', }, hljs.inherit(VALUE_CONTAINER, {begin: /:/}) ].concat(ALLOWED_COMMENTS), illegal: '\\S' }; var ARRAY = { begin: '\\[', end: '\\]', contains: [hljs.inherit(VALUE_CONTAINER)], // inherit is a workaround for a bug that makes shared modes with endsWithParent compile only the ending of one of the parents illegal: '\\S' }; TYPES.push(OBJECT, ARRAY); ALLOWED_COMMENTS.forEach(function(rule) { TYPES.push(rule) }) return { name: 'JSON', contains: TYPES, keywords: LITERALS, illegal: '\\S' }; } ================================================ FILE: miniprogram/components/towxml/parse/highlight/languages/less.js ================================================ /* Language: Less Description: It's CSS, with just a little more. Author: Max Mikhailov Website: http://lesscss.org Category: common, css */ export default function(hljs) { var IDENT_RE = '[\\w-]+'; // yes, Less identifiers may begin with a digit var INTERP_IDENT_RE = '(' + IDENT_RE + '|@{' + IDENT_RE + '})'; /* Generic Modes */ var RULES = [], VALUE = []; // forward def. for recursive modes var STRING_MODE = function(c) { return { // Less strings are not multiline (also include '~' for more consistent coloring of "escaped" strings) className: 'string', begin: '~?' + c + '.*?' + c };}; var IDENT_MODE = function(name, begin, relevance) { return { className: name, begin: begin, relevance: relevance };}; var PARENS_MODE = { // used only to properly balance nested parens inside mixin call, def. arg list begin: '\\(', end: '\\)', contains: VALUE, relevance: 0 }; // generic Less highlighter (used almost everywhere except selectors): VALUE.push( hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, STRING_MODE("'"), STRING_MODE('"'), hljs.CSS_NUMBER_MODE, // fixme: it does not include dot for numbers like .5em :( { begin: '(url|data-uri)\\(', starts: {className: 'string', end: '[\\)\\n]', excludeEnd: true} }, IDENT_MODE('number', '#[0-9A-Fa-f]+\\b'), PARENS_MODE, IDENT_MODE('variable', '@@?' + IDENT_RE, 10), IDENT_MODE('variable', '@{' + IDENT_RE + '}'), IDENT_MODE('built_in', '~?`[^`]*?`'), // inline javascript (or whatever host language) *multiline* string { // @media features (it’s here to not duplicate things in AT_RULE_MODE with extra PARENS_MODE overriding): className: 'attribute', begin: IDENT_RE + '\\s*:', end: ':', returnBegin: true, excludeEnd: true }, { className: 'meta', begin: '!important' } ); var VALUE_WITH_RULESETS = VALUE.concat({ begin: '{', end: '}', contains: RULES }); var MIXIN_GUARD_MODE = { beginKeywords: 'when', endsWithParent: true, contains: [{beginKeywords: 'and not'}].concat(VALUE) // using this form to override VALUE’s 'function' match }; /* Rule-Level Modes */ var RULE_MODE = { begin: INTERP_IDENT_RE + '\\s*:', returnBegin: true, end: '[;}]', relevance: 0, contains: [ { className: 'attribute', begin: INTERP_IDENT_RE, end: ':', excludeEnd: true, starts: { endsWithParent: true, illegal: '[<=$]', relevance: 0, contains: VALUE } } ] }; var AT_RULE_MODE = { className: 'keyword', begin: '@(import|media|charset|font-face|(-[a-z]+-)?keyframes|supports|document|namespace|page|viewport|host)\\b', starts: {end: '[;{}]', returnEnd: true, contains: VALUE, relevance: 0} }; // variable definitions and calls var VAR_RULE_MODE = { className: 'variable', variants: [ // using more strict pattern for higher relevance to increase chances of Less detection. // this is *the only* Less specific statement used in most of the sources, so... // (we’ll still often loose to the css-parser unless there's '//' comment, // simply because 1 variable just can't beat 99 properties :) {begin: '@' + IDENT_RE + '\\s*:', relevance: 15}, {begin: '@' + IDENT_RE} ], starts: {end: '[;}]', returnEnd: true, contains: VALUE_WITH_RULESETS} }; var SELECTOR_MODE = { // first parse unambiguous selectors (i.e. those not starting with tag) // then fall into the scary lookahead-discriminator variant. // this mode also handles mixin definitions and calls variants: [{ begin: '[\\.#:&\\[>]', end: '[;{}]' // mixin calls end with ';' }, { begin: INTERP_IDENT_RE, end: '{' }], returnBegin: true, returnEnd: true, illegal: '[<=\'$"]', relevance: 0, contains: [ hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, MIXIN_GUARD_MODE, IDENT_MODE('keyword', 'all\\b'), IDENT_MODE('variable', '@{' + IDENT_RE + '}'), // otherwise it’s identified as tag IDENT_MODE('selector-tag', INTERP_IDENT_RE + '%?', 0), // '%' for more consistent coloring of @keyframes "tags" IDENT_MODE('selector-id', '#' + INTERP_IDENT_RE), IDENT_MODE('selector-class', '\\.' + INTERP_IDENT_RE, 0), IDENT_MODE('selector-tag', '&', 0), {className: 'selector-attr', begin: '\\[', end: '\\]'}, {className: 'selector-pseudo', begin: /:(:)?[a-zA-Z0-9\_\-\+\(\)"'.]+/}, {begin: '\\(', end: '\\)', contains: VALUE_WITH_RULESETS}, // argument list of parametric mixins {begin: '!important'} // eat !important after mixin call or it will be colored as tag ] }; RULES.push( hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, AT_RULE_MODE, VAR_RULE_MODE, RULE_MODE, SELECTOR_MODE ); return { name: 'Less', case_insensitive: true, illegal: '[=>\'/<($"]', contains: RULES }; } ================================================ FILE: miniprogram/components/towxml/parse/highlight/languages/nginx.js ================================================ /* Language: Nginx config Author: Peter Leonov Contributors: Ivan Sagalaev Category: common, config Website: https://www.nginx.com */ export default function(hljs) { var VAR = { className: 'variable', variants: [ {begin: /\$\d+/}, {begin: /\$\{/, end: /}/}, {begin: '[\\$\\@]' + hljs.UNDERSCORE_IDENT_RE} ] }; var DEFAULT = { endsWithParent: true, lexemes: '[a-z/_]+', keywords: { literal: 'on off yes no true false none blocked debug info notice warn error crit ' + 'select break last permanent redirect kqueue rtsig epoll poll /dev/poll' }, relevance: 0, illegal: '=>', contains: [ hljs.HASH_COMMENT_MODE, { className: 'string', contains: [hljs.BACKSLASH_ESCAPE, VAR], variants: [ {begin: /"/, end: /"/}, {begin: /'/, end: /'/} ] }, // this swallows entire URLs to avoid detecting numbers within { begin: '([a-z]+):/', end: '\\s', endsWithParent: true, excludeEnd: true, contains: [VAR] }, { className: 'regexp', contains: [hljs.BACKSLASH_ESCAPE, VAR], variants: [ {begin: "\\s\\^", end: "\\s|{|;", returnEnd: true}, // regexp locations (~, ~*) {begin: "~\\*?\\s+", end: "\\s|{|;", returnEnd: true}, // *.example.com {begin: "\\*(\\.[a-z\\-]+)+"}, // sub.example.* {begin: "([a-z\\-]+\\.)+\\*"} ] }, // IP { className: 'number', begin: '\\b\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}(:\\d{1,5})?\\b' }, // units { className: 'number', begin: '\\b\\d+[kKmMgGdshdwy]*\\b', relevance: 0 }, VAR ] }; return { name: 'Nginx config', aliases: ['nginxconf'], contains: [ hljs.HASH_COMMENT_MODE, { begin: hljs.UNDERSCORE_IDENT_RE + '\\s+{', returnBegin: true, end: '{', contains: [ { className: 'section', begin: hljs.UNDERSCORE_IDENT_RE } ], relevance: 0 }, { begin: hljs.UNDERSCORE_IDENT_RE + '\\s', end: ';|{', returnBegin: true, contains: [ { className: 'attribute', begin: hljs.UNDERSCORE_IDENT_RE, starts: DEFAULT } ], relevance: 0 } ], illegal: '[^\\s\\}]' }; } ================================================ FILE: miniprogram/components/towxml/parse/highlight/languages/php.js ================================================ /* Language: PHP Author: Victor Karamzin Contributors: Evgeny Stepanischev , Ivan Sagalaev Website: https://www.php.net Category: common */ export default function(hljs) { var VARIABLE = { begin: '\\$+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*' }; var PREPROCESSOR = { className: 'meta', variants: [ { begin: /<\?php/, relevance: 10 }, // boost for obvious PHP { begin: /<\?[=]?/ }, { begin: /\?>/ } // end php tag ] }; var STRING = { className: 'string', contains: [hljs.BACKSLASH_ESCAPE, PREPROCESSOR], variants: [ { begin: 'b"', end: '"' }, { begin: 'b\'', end: '\'' }, hljs.inherit(hljs.APOS_STRING_MODE, {illegal: null}), hljs.inherit(hljs.QUOTE_STRING_MODE, {illegal: null}) ] }; var NUMBER = {variants: [hljs.BINARY_NUMBER_MODE, hljs.C_NUMBER_MODE]}; var KEYWORDS = { keyword: // Magic constants: // '__CLASS__ __DIR__ __FILE__ __FUNCTION__ __LINE__ __METHOD__ __NAMESPACE__ __TRAIT__ ' + // Function that look like language construct or language construct that look like function: // List of keywords that may not require parenthesis 'die echo exit include include_once print require require_once ' + // These are not language construct (function) but operate on the currently-executing function and can access the current symbol table // 'compact extract func_get_arg func_get_args func_num_args get_called_class get_parent_class ' + // Other keywords: // // 'array abstract and as binary bool boolean break callable case catch class clone const continue declare default do double else elseif empty enddeclare endfor endforeach endif endswitch endwhile eval extends final finally float for foreach from global goto if implements instanceof insteadof int integer interface isset iterable list new object or private protected public real return string switch throw trait try unset use var void while xor yield', literal: 'false null true', built_in: // Standard PHP library: // 'Error|0 ' + // error is too common a name esp since PHP is case in-sensitive 'AppendIterator ArgumentCountError ArithmeticError ArrayIterator ArrayObject AssertionError BadFunctionCallException BadMethodCallException CachingIterator CallbackFilterIterator CompileError Countable DirectoryIterator DivisionByZeroError DomainException EmptyIterator ErrorException Exception FilesystemIterator FilterIterator GlobIterator InfiniteIterator InvalidArgumentException IteratorIterator LengthException LimitIterator LogicException MultipleIterator NoRewindIterator OutOfBoundsException OutOfRangeException OuterIterator OverflowException ParentIterator ParseError RangeException RecursiveArrayIterator RecursiveCachingIterator RecursiveCallbackFilterIterator RecursiveDirectoryIterator RecursiveFilterIterator RecursiveIterator RecursiveIteratorIterator RecursiveRegexIterator RecursiveTreeIterator RegexIterator RuntimeException SeekableIterator SplDoublyLinkedList SplFileInfo SplFileObject SplFixedArray SplHeap SplMaxHeap SplMinHeap SplObjectStorage SplObserver SplObserver SplPriorityQueue SplQueue SplStack SplSubject SplSubject SplTempFileObject TypeError UnderflowException UnexpectedValueException ' + // Reserved interfaces: // 'ArrayAccess Closure Generator Iterator IteratorAggregate Serializable Throwable Traversable WeakReference ' + // Reserved classes: // 'Directory __PHP_Incomplete_Class parent php_user_filter self static stdClass' }; return { aliases: ['php', 'php3', 'php4', 'php5', 'php6', 'php7'], case_insensitive: true, keywords: KEYWORDS, contains: [ hljs.HASH_COMMENT_MODE, hljs.COMMENT('//', '$', {contains: [PREPROCESSOR]}), hljs.COMMENT( '/\\*', '\\*/', { contains: [ { className: 'doctag', begin: '@[A-Za-z]+' } ] } ), hljs.COMMENT( '__halt_compiler.+?;', false, { endsWithParent: true, keywords: '__halt_compiler', lexemes: hljs.UNDERSCORE_IDENT_RE } ), { className: 'string', begin: /<<<['"]?\w+['"]?$/, end: /^\w+;?$/, contains: [ hljs.BACKSLASH_ESCAPE, { className: 'subst', variants: [ {begin: /\$\w+/}, {begin: /\{\$/, end: /\}/} ] } ] }, PREPROCESSOR, { className: 'keyword', begin: /\$this\b/ }, VARIABLE, { // swallow composed identifiers to avoid parsing them as keywords begin: /(::|->)+[a-zA-Z_\x7f-\xff][a-zA-Z0-9_\x7f-\xff]*/ }, { className: 'function', beginKeywords: 'fn function', end: /[;{]/, excludeEnd: true, illegal: '[$%\\[]', contains: [ hljs.UNDERSCORE_TITLE_MODE, { className: 'params', begin: '\\(', end: '\\)', excludeBegin: true, excludeEnd: true, keywords: KEYWORDS, contains: [ 'self', VARIABLE, hljs.C_BLOCK_COMMENT_MODE, STRING, NUMBER ] } ] }, { className: 'class', beginKeywords: 'class interface', end: '{', excludeEnd: true, illegal: /[:\(\$"]/, contains: [ {beginKeywords: 'extends implements'}, hljs.UNDERSCORE_TITLE_MODE ] }, { beginKeywords: 'namespace', end: ';', illegal: /[\.']/, contains: [hljs.UNDERSCORE_TITLE_MODE] }, { beginKeywords: 'use', end: ';', contains: [hljs.UNDERSCORE_TITLE_MODE] }, { begin: '=>' // No markup, just a relevance booster }, STRING, NUMBER ] }; } ================================================ FILE: miniprogram/components/towxml/parse/highlight/languages/python-repl.js ================================================ /* Language: Python REPL Requires: python.js Author: Josh Goebel Category: common */ export default function(hljs) { return { aliases: ['pycon'], contains: [ { className: 'meta', starts: { // a space separates the REPL prefix from the actual code // this is purely for cleaner HTML output end: / |$/, starts: { end: '$', subLanguage: 'python' } }, variants: [ { begin: /^>>>(?=[ ]|$)/ }, { begin: /^\.\.\.(?=[ ]|$)/ } ] }, ] } } ================================================ FILE: miniprogram/components/towxml/parse/highlight/languages/python.js ================================================ /* Language: Python Description: Python is an interpreted, object-oriented, high-level programming language with dynamic semantics. Website: https://www.python.org Category: common */ export default function(hljs) { var KEYWORDS = { keyword: 'and elif is global as in if from raise for except finally print import pass return ' + 'exec else break not with class assert yield try while continue del or def lambda ' + 'async await nonlocal|10', built_in: 'Ellipsis NotImplemented', literal: 'False None True' }; var PROMPT = { className: 'meta', begin: /^(>>>|\.\.\.) / }; var SUBST = { className: 'subst', begin: /\{/, end: /\}/, keywords: KEYWORDS, illegal: /#/ }; var LITERAL_BRACKET = { begin: /\{\{/, relevance: 0 }; var STRING = { className: 'string', contains: [hljs.BACKSLASH_ESCAPE], variants: [ { begin: /(u|b)?r?'''/, end: /'''/, contains: [hljs.BACKSLASH_ESCAPE, PROMPT], relevance: 10 }, { begin: /(u|b)?r?"""/, end: /"""/, contains: [hljs.BACKSLASH_ESCAPE, PROMPT], relevance: 10 }, { begin: /(fr|rf|f)'''/, end: /'''/, contains: [hljs.BACKSLASH_ESCAPE, PROMPT, LITERAL_BRACKET, SUBST] }, { begin: /(fr|rf|f)"""/, end: /"""/, contains: [hljs.BACKSLASH_ESCAPE, PROMPT, LITERAL_BRACKET, SUBST] }, { begin: /(u|r|ur)'/, end: /'/, relevance: 10 }, { begin: /(u|r|ur)"/, end: /"/, relevance: 10 }, { begin: /(b|br)'/, end: /'/ }, { begin: /(b|br)"/, end: /"/ }, { begin: /(fr|rf|f)'/, end: /'/, contains: [hljs.BACKSLASH_ESCAPE, LITERAL_BRACKET, SUBST] }, { begin: /(fr|rf|f)"/, end: /"/, contains: [hljs.BACKSLASH_ESCAPE, LITERAL_BRACKET, SUBST] }, hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE ] }; var NUMBER = { className: 'number', relevance: 0, variants: [ {begin: hljs.BINARY_NUMBER_RE + '[lLjJ]?'}, {begin: '\\b(0o[0-7]+)[lLjJ]?'}, {begin: hljs.C_NUMBER_RE + '[lLjJ]?'} ] }; var PARAMS = { className: 'params', begin: /\(/, end: /\)/, contains: ['self', PROMPT, NUMBER, STRING, hljs.HASH_COMMENT_MODE] }; SUBST.contains = [STRING, NUMBER, PROMPT]; return { name: 'Python', aliases: ['py', 'gyp', 'ipython'], keywords: KEYWORDS, illegal: /(<\/|->|\?)|=>/, contains: [ PROMPT, NUMBER, // eat "if" prior to string so that it won't accidentally be // labeled as an f-string as in: { beginKeywords: "if", relevance: 0 }, STRING, hljs.HASH_COMMENT_MODE, { variants: [ {className: 'function', beginKeywords: 'def'}, {className: 'class', beginKeywords: 'class'} ], end: /:/, illegal: /[${=;\n,]/, contains: [ hljs.UNDERSCORE_TITLE_MODE, PARAMS, { begin: /->/, endsWithParent: true, keywords: 'None' } ] }, { className: 'meta', begin: /^[\t ]*@/, end: /$/ }, { begin: /\b(print|exec)\(/ // don’t highlight keywords-turned-functions in Python 3 } ] }; } ================================================ FILE: miniprogram/components/towxml/parse/highlight/languages/scss.js ================================================ /* Language: SCSS Description: Scss is an extension of the syntax of CSS. Author: Kurt Emch Website: https://sass-lang.com Category: common, css */ export default function(hljs) { var AT_IDENTIFIER = '@[a-z-]+' // @font-face var AT_MODIFIERS = "and or not only" var IDENT_RE = '[a-zA-Z-][a-zA-Z0-9_-]*'; var VARIABLE = { className: 'variable', begin: '(\\$' + IDENT_RE + ')\\b' }; var HEXCOLOR = { className: 'number', begin: '#[0-9A-Fa-f]+' }; var DEF_INTERNALS = { className: 'attribute', begin: '[A-Z\\_\\.\\-]+', end: ':', excludeEnd: true, illegal: '[^\\s]', starts: { endsWithParent: true, excludeEnd: true, contains: [ HEXCOLOR, hljs.CSS_NUMBER_MODE, hljs.QUOTE_STRING_MODE, hljs.APOS_STRING_MODE, hljs.C_BLOCK_COMMENT_MODE, { className: 'meta', begin: '!important' } ] } }; return { name: 'SCSS', case_insensitive: true, illegal: '[=/|\']', contains: [ hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, { className: 'selector-id', begin: '\\#[A-Za-z0-9_-]+', relevance: 0 }, { className: 'selector-class', begin: '\\.[A-Za-z0-9_-]+', relevance: 0 }, { className: 'selector-attr', begin: '\\[', end: '\\]', illegal: '$' }, { className: 'selector-tag', // begin: IDENT_RE, end: '[,|\\s]' begin: '\\b(a|abbr|acronym|address|area|article|aside|audio|b|base|big|blockquote|body|br|button|canvas|caption|cite|code|col|colgroup|command|datalist|dd|del|details|dfn|div|dl|dt|em|embed|fieldset|figcaption|figure|footer|form|frame|frameset|(h[1-6])|head|header|hgroup|hr|html|i|iframe|img|input|ins|kbd|keygen|label|legend|li|link|map|mark|meta|meter|nav|noframes|noscript|object|ol|optgroup|option|output|p|param|pre|progress|q|rp|rt|ruby|samp|script|section|select|small|span|strike|strong|style|sub|sup|table|tbody|td|textarea|tfoot|th|thead|time|title|tr|tt|ul|var|video)\\b', relevance: 0 }, { className: 'selector-pseudo', begin: ':(visited|valid|root|right|required|read-write|read-only|out-range|optional|only-of-type|only-child|nth-of-type|nth-last-of-type|nth-last-child|nth-child|not|link|left|last-of-type|last-child|lang|invalid|indeterminate|in-range|hover|focus|first-of-type|first-line|first-letter|first-child|first|enabled|empty|disabled|default|checked|before|after|active)' }, { className: 'selector-pseudo', begin: '::(after|before|choices|first-letter|first-line|repeat-index|repeat-item|selection|value)' }, VARIABLE, { className: 'attribute', begin: '\\b(src|z-index|word-wrap|word-spacing|word-break|width|widows|white-space|visibility|vertical-align|unicode-bidi|transition-timing-function|transition-property|transition-duration|transition-delay|transition|transform-style|transform-origin|transform|top|text-underline-position|text-transform|text-shadow|text-rendering|text-overflow|text-indent|text-decoration-style|text-decoration-line|text-decoration-color|text-decoration|text-align-last|text-align|tab-size|table-layout|right|resize|quotes|position|pointer-events|perspective-origin|perspective|page-break-inside|page-break-before|page-break-after|padding-top|padding-right|padding-left|padding-bottom|padding|overflow-y|overflow-x|overflow-wrap|overflow|outline-width|outline-style|outline-offset|outline-color|outline|orphans|order|opacity|object-position|object-fit|normal|none|nav-up|nav-right|nav-left|nav-index|nav-down|min-width|min-height|max-width|max-height|mask|marks|margin-top|margin-right|margin-left|margin-bottom|margin|list-style-type|list-style-position|list-style-image|list-style|line-height|letter-spacing|left|justify-content|initial|inherit|ime-mode|image-orientation|image-resolution|image-rendering|icon|hyphens|height|font-weight|font-variant-ligatures|font-variant|font-style|font-stretch|font-size-adjust|font-size|font-language-override|font-kerning|font-feature-settings|font-family|font|float|flex-wrap|flex-shrink|flex-grow|flex-flow|flex-direction|flex-basis|flex|filter|empty-cells|display|direction|cursor|counter-reset|counter-increment|content|column-width|column-span|column-rule-width|column-rule-style|column-rule-color|column-rule|column-gap|column-fill|column-count|columns|color|clip-path|clip|clear|caption-side|break-inside|break-before|break-after|box-sizing|box-shadow|box-decoration-break|bottom|border-width|border-top-width|border-top-style|border-top-right-radius|border-top-left-radius|border-top-color|border-top|border-style|border-spacing|border-right-width|border-right-style|border-right-color|border-right|border-radius|border-left-width|border-left-style|border-left-color|border-left|border-image-width|border-image-source|border-image-slice|border-image-repeat|border-image-outset|border-image|border-color|border-collapse|border-bottom-width|border-bottom-style|border-bottom-right-radius|border-bottom-left-radius|border-bottom-color|border-bottom|border|background-size|background-repeat|background-position|background-origin|background-image|background-color|background-clip|background-attachment|background-blend-mode|background|backface-visibility|auto|animation-timing-function|animation-play-state|animation-name|animation-iteration-count|animation-fill-mode|animation-duration|animation-direction|animation-delay|animation|align-self|align-items|align-content)\\b', illegal: '[^\\s]' }, { begin: '\\b(whitespace|wait|w-resize|visible|vertical-text|vertical-ideographic|uppercase|upper-roman|upper-alpha|underline|transparent|top|thin|thick|text|text-top|text-bottom|tb-rl|table-header-group|table-footer-group|sw-resize|super|strict|static|square|solid|small-caps|separate|se-resize|scroll|s-resize|rtl|row-resize|ridge|right|repeat|repeat-y|repeat-x|relative|progress|pointer|overline|outside|outset|oblique|nowrap|not-allowed|normal|none|nw-resize|no-repeat|no-drop|newspaper|ne-resize|n-resize|move|middle|medium|ltr|lr-tb|lowercase|lower-roman|lower-alpha|loose|list-item|line|line-through|line-edge|lighter|left|keep-all|justify|italic|inter-word|inter-ideograph|inside|inset|inline|inline-block|inherit|inactive|ideograph-space|ideograph-parenthesis|ideograph-numeric|ideograph-alpha|horizontal|hidden|help|hand|groove|fixed|ellipsis|e-resize|double|dotted|distribute|distribute-space|distribute-letter|distribute-all-lines|disc|disabled|default|decimal|dashed|crosshair|collapse|col-resize|circle|char|center|capitalize|break-word|break-all|bottom|both|bolder|bold|block|bidi-override|below|baseline|auto|always|all-scroll|absolute|table|table-cell)\\b' }, { begin: ':', end: ';', contains: [ VARIABLE, HEXCOLOR, hljs.CSS_NUMBER_MODE, hljs.QUOTE_STRING_MODE, hljs.APOS_STRING_MODE, { className: 'meta', begin: '!important' } ] }, // matching these here allows us to treat them more like regular CSS // rules so everything between the {} gets regular rule highlighting, // which is what we want for page and font-face { begin: '@(page|font-face)', lexemes: AT_IDENTIFIER, keywords: '@page @font-face' }, { begin: '@', end: '[{;]', returnBegin: true, keywords: AT_MODIFIERS, contains: [ { begin: AT_IDENTIFIER, className: "keyword" }, VARIABLE, hljs.QUOTE_STRING_MODE, hljs.APOS_STRING_MODE, HEXCOLOR, hljs.CSS_NUMBER_MODE, // { // begin: '\\s[A-Za-z0-9_.-]+', // relevance: 0 // } ] } ] }; } ================================================ FILE: miniprogram/components/towxml/parse/highlight/languages/shell.js ================================================ /* Language: Shell Session Requires: bash.js Author: TSUYUSATO Kitsune Category: common */ export default function(hljs) { return { name: 'Shell Session', aliases: ['console'], contains: [ { className: 'meta', begin: '^\\s{0,3}[/\\w\\d\\[\\]()@-]*[>%$#]', starts: { end: '$', subLanguage: 'bash' } } ] } } ================================================ FILE: miniprogram/components/towxml/parse/highlight/languages/typescript.js ================================================ /* Language: TypeScript Author: Panu Horsmalahti Contributors: Ike Ku Description: TypeScript is a strict superset of JavaScript Website: https://www.typescriptlang.org Category: common, scripting */ export default function(hljs) { var JS_IDENT_RE = '[A-Za-z$_][0-9A-Za-z$_]*'; var KEYWORDS = { keyword: 'in if for while finally var new function do return void else break catch ' + 'instanceof with throw case default try this switch continue typeof delete ' + 'let yield const class public private protected get set super ' + 'static implements enum export import declare type namespace abstract ' + 'as from extends async await', literal: 'true false null undefined NaN Infinity', built_in: 'eval isFinite isNaN parseFloat parseInt decodeURI decodeURIComponent ' + 'encodeURI encodeURIComponent escape unescape Object Function Boolean Error ' + 'EvalError InternalError RangeError ReferenceError StopIteration SyntaxError ' + 'TypeError URIError Number Math Date String RegExp Array Float32Array ' + 'Float64Array Int16Array Int32Array Int8Array Uint16Array Uint32Array ' + 'Uint8Array Uint8ClampedArray ArrayBuffer DataView JSON Intl arguments require ' + 'module console window document any number boolean string void Promise' }; var DECORATOR = { className: 'meta', begin: '@' + JS_IDENT_RE, }; var ARGS = { begin: '\\(', end: /\)/, keywords: KEYWORDS, contains: [ 'self', hljs.QUOTE_STRING_MODE, hljs.APOS_STRING_MODE, hljs.NUMBER_MODE ] }; var PARAMS = { className: 'params', begin: /\(/, end: /\)/, excludeBegin: true, excludeEnd: true, keywords: KEYWORDS, contains: [ hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, DECORATOR, ARGS ] }; var NUMBER = { className: 'number', variants: [ { begin: '\\b(0[bB][01]+)n?' }, { begin: '\\b(0[oO][0-7]+)n?' }, { begin: hljs.C_NUMBER_RE + 'n?' } ], relevance: 0 }; var SUBST = { className: 'subst', begin: '\\$\\{', end: '\\}', keywords: KEYWORDS, contains: [] // defined later }; var HTML_TEMPLATE = { begin: 'html`', end: '', starts: { end: '`', returnEnd: false, contains: [ hljs.BACKSLASH_ESCAPE, SUBST ], subLanguage: 'xml', } }; var CSS_TEMPLATE = { begin: 'css`', end: '', starts: { end: '`', returnEnd: false, contains: [ hljs.BACKSLASH_ESCAPE, SUBST ], subLanguage: 'css', } }; var TEMPLATE_STRING = { className: 'string', begin: '`', end: '`', contains: [ hljs.BACKSLASH_ESCAPE, SUBST ] }; SUBST.contains = [ hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, HTML_TEMPLATE, CSS_TEMPLATE, TEMPLATE_STRING, NUMBER, hljs.REGEXP_MODE ]; return { name: 'TypeScript', aliases: ['ts'], keywords: KEYWORDS, contains: [ { className: 'meta', begin: /^\s*['"]use strict['"]/ }, hljs.APOS_STRING_MODE, hljs.QUOTE_STRING_MODE, HTML_TEMPLATE, CSS_TEMPLATE, TEMPLATE_STRING, hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, NUMBER, { // "value" container begin: '(' + hljs.RE_STARTERS_RE + '|\\b(case|return|throw)\\b)\\s*', keywords: 'return throw case', contains: [ hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE, hljs.REGEXP_MODE, { className: 'function', begin: '(\\(.*?\\)|' + hljs.IDENT_RE + ')\\s*=>', returnBegin: true, end: '\\s*=>', contains: [ { className: 'params', variants: [ { begin: hljs.IDENT_RE }, { begin: /\(\s*\)/, }, { begin: /\(/, end: /\)/, excludeBegin: true, excludeEnd: true, keywords: KEYWORDS, contains: [ 'self', hljs.C_LINE_COMMENT_MODE, hljs.C_BLOCK_COMMENT_MODE ] } ] } ] } ], relevance: 0 }, { className: 'function', beginKeywords: 'function', end: /[\{;]/, excludeEnd: true, keywords: KEYWORDS, contains: [ 'self', hljs.inherit(hljs.TITLE_MODE, { begin: JS_IDENT_RE }), PARAMS ], illegal: /%/, relevance: 0 // () => {} is more typical in TypeScript }, { beginKeywords: 'constructor', end: /[\{;]/, excludeEnd: true, contains: [ 'self', PARAMS ] }, { // prevent references like module.id from being higlighted as module definitions begin: /module\./, keywords: { built_in: 'module' }, relevance: 0 }, { beginKeywords: 'module', end: /\{/, excludeEnd: true }, { beginKeywords: 'interface', end: /\{/, excludeEnd: true, keywords: 'interface extends' }, { begin: /\$[(.]/ // relevance booster for a pattern common to JS libs: `$(something)` and `$.something` }, { begin: '\\.' + hljs.IDENT_RE, relevance: 0 // hack: prevents detection of keywords after dots }, DECORATOR, ARGS ] }; } ================================================ FILE: miniprogram/components/towxml/parse/highlight/languages/xml.js ================================================ /* Language: HTML, XML Website: https://www.w3.org/XML/ Category: common */ export default function(hljs) { var XML_IDENT_RE = '[A-Za-z0-9\\._:-]+'; var XML_ENTITIES = { className: 'symbol', begin: '&[a-z]+;|&#[0-9]+;|&#x[a-f0-9]+;' }; var XML_META_KEYWORDS = { begin: '\\s', contains:[ { className: 'meta-keyword', begin: '#?[a-z_][a-z1-9_-]+', illegal: '\\n', } ] }; var XML_META_PAR_KEYWORDS = hljs.inherit(XML_META_KEYWORDS, {begin: '\\(', end: '\\)'}); var APOS_META_STRING_MODE = hljs.inherit(hljs.APOS_STRING_MODE, {className: 'meta-string'}); var QUOTE_META_STRING_MODE = hljs.inherit(hljs.QUOTE_STRING_MODE, {className: 'meta-string'}); var TAG_INTERNALS = { endsWithParent: true, illegal: /`]+/} ] } ] } ] }; return { name: 'HTML, XML', aliases: ['html', 'xhtml', 'rss', 'atom', 'xjb', 'xsd', 'xsl', 'plist', 'wsf', 'svg'], case_insensitive: true, contains: [ { className: 'meta', begin: '', relevance: 10, contains: [ XML_META_KEYWORDS, QUOTE_META_STRING_MODE, APOS_META_STRING_MODE, XML_META_PAR_KEYWORDS, { begin: '\\[', end: '\\]', contains:[ { className: 'meta', begin: '', contains: [ XML_META_KEYWORDS, XML_META_PAR_KEYWORDS, QUOTE_META_STRING_MODE, APOS_META_STRING_MODE ] } ] } ] }, hljs.COMMENT( '', { relevance: 10 } ), { begin: '<\\!\\[CDATA\\[', end: '\\]\\]>', relevance: 10 }, XML_ENTITIES, { className: 'meta', begin: /<\?xml/, end: /\?>/, relevance: 10 }, { className: 'tag', /* The lookahead pattern (?=...) ensures that 'begin' only matches ')', end: '>', keywords: {name: 'style'}, contains: [TAG_INTERNALS], starts: { end: '', returnEnd: true, subLanguage: ['css', 'xml'] } }, { className: 'tag', // See the comment in the