├── .browserslistrc ├── .distignore ├── .editorconfig ├── .eslintrc.json ├── .github └── workflows │ ├── node.js.yml │ └── publish.yml ├── .gitignore ├── .idea ├── .gitignore ├── codeStyles │ ├── Project.xml │ └── codeStyleConfig.xml ├── externalDependencies.xml ├── git_toolbox_prj.xml ├── jsLibraryMappings.xml ├── modules.xml ├── obsidian-web-clipper.iml ├── runConfigurations │ ├── watch.xml │ ├── watch_angular.xml │ └── watch_package.xml ├── vcs.xml └── watcherTasks.xml ├── .prettierignore ├── .prettierrc ├── .yarn ├── plugins │ └── @yarnpkg │ │ ├── plugin-interactive-tools.cjs │ │ ├── plugin-typescript.cjs │ │ └── plugin-version.cjs └── releases │ └── yarn-3.3.0.cjs ├── .yarnrc.yml ├── LICENSE ├── README.md ├── angular.json ├── package.json ├── src ├── _locales │ ├── en │ │ └── messages.json │ └── zh_CN │ │ └── messages.json ├── action.ts ├── app │ ├── app-routing.module.ts │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.ts │ ├── app.module.ts │ ├── article-extractor.service.ts │ ├── background │ │ ├── background-routing.module.ts │ │ ├── background.component.ts │ │ ├── background.module.ts │ │ ├── markdown.service.ts │ │ └── obsidian.service.ts │ ├── export-template.service.ts │ ├── extension.service.ts │ ├── i18n.pipe.ts │ ├── option.service.ts │ ├── options │ │ ├── general │ │ │ ├── general.component.html │ │ │ ├── general.component.scss │ │ │ └── general.component.ts │ │ ├── options-routing.module.ts │ │ ├── options.component.html │ │ ├── options.component.scss │ │ ├── options.component.ts │ │ ├── options.module.ts │ │ ├── rules │ │ │ ├── rules-routing.module.ts │ │ │ ├── rules.component.html │ │ │ ├── rules.component.scss │ │ │ ├── rules.component.ts │ │ │ └── rules.module.ts │ │ └── shortcuts │ │ │ ├── shortcuts.component.html │ │ │ ├── shortcuts.component.scss │ │ │ └── shortcuts.component.ts │ ├── rule.service.ts │ └── shared.module.ts ├── assets │ ├── .gitkeep │ └── default.template ├── content-scripts │ ├── background-listener.ts │ ├── browser.ts │ ├── error-action.ts │ ├── export-action.ts │ ├── index.ts │ ├── shortcuts.ts │ └── utils.ts ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── main.ts ├── manifest.json ├── polyfills.ts ├── production │ └── manifest.json ├── styles.scss └── template.d.ts ├── tsconfig.app.json ├── tsconfig.json ├── webpack.externals.cjs └── yarn.lock /.browserslistrc: -------------------------------------------------------------------------------- 1 | last 3 Firefox version 2 | Firefox ESR 3 | -------------------------------------------------------------------------------- /.distignore: -------------------------------------------------------------------------------- 1 | .git 2 | .github 3 | node_modules 4 | dist 5 | 6 | .yarn/install-state.gz 7 | .yarn/cache 8 | yarn-error.log 9 | 10 | .idea 11 | web-ext-artifacts 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.ts] 12 | quote_type = single 13 | 14 | [*.md] 15 | max_line_length = off 16 | trim_trailing_whitespace = false 17 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "ignorePatterns": [ 4 | "projects/**/*" 5 | ], 6 | "overrides": [ 7 | { 8 | "files": [ 9 | "*.ts" 10 | ], 11 | "extends": [ 12 | "eslint:recommended", 13 | "plugin:@typescript-eslint/recommended", 14 | "plugin:unicorn/recommended", 15 | "plugin:prettier/recommended", 16 | "plugin:rxjs/recommended", 17 | "plugin:@angular-eslint/recommended" 18 | ], 19 | "rules": { 20 | "@angular-eslint/directive-selector": [ 21 | "error", 22 | { 23 | "type": "attribute", 24 | "prefix": "app", 25 | "style": "camelCase" 26 | } 27 | ], 28 | "@angular-eslint/component-selector": [ 29 | "error", 30 | { 31 | "type": "element", 32 | "prefix": "app", 33 | "style": "kebab-case" 34 | } 35 | ] 36 | } 37 | }, 38 | { 39 | "files": [ 40 | "*.html" 41 | ], 42 | "extends": [ 43 | "plugin:@angular-eslint/template/recommended" 44 | ], 45 | "rules": {} 46 | } 47 | ], 48 | "parserOptions": { 49 | "project": "./tsconfig.json" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Use Node.js 16 15 | uses: actions/setup-node@v2 16 | with: 17 | node-version: 16 18 | cache: 'yarn' 19 | - run: yarn 20 | - run: yarn build:angular 21 | - run: yarn build:content 22 | - run: yarn lint 23 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Firefox Addon 2 | 3 | on: 4 | workflow_dispatch: 5 | release: 6 | types: 7 | - released 8 | 9 | jobs: 10 | publish: 11 | runs-on: ubuntu-latest 12 | steps: 13 | - uses: actions/checkout@v3 14 | - name: Use Node.js 16 15 | uses: actions/setup-node@v2 16 | with: 17 | node-version: 16 18 | cache: yarn 19 | - name: Compress src 20 | id: compress-src 21 | uses: byteever/action-build-zip@master 22 | with: 23 | filename: 'src.zip' 24 | - run: yarn 25 | - run: yarn build:angular 26 | - run: yarn build:content 27 | - run: yarn lint 28 | - name: "web-ext build" 29 | id: web-ext-build 30 | uses: kewisch/action-web-ext@v1 31 | with: 32 | cmd: build 33 | source: dist 34 | filename: "{name}-{version}.xpi" 35 | - name: "Upload Artifact" 36 | uses: actions/upload-artifact@v3 37 | with: 38 | name: addon.xpi 39 | path: ${{ steps.web-ext-build.outputs.target }} 40 | - name: "Publish" 41 | uses: SettingDust/publish-firefox-addon-action@master 42 | with: 43 | addonId: 'dust-obsidian-web-clipper' 44 | jwtIssuer: ${{ secrets.FIREFOX_JWT_ISSUER }} 45 | jwtSecret: ${{ secrets.FIREFOX_JWT_SECRET }} 46 | addonFile: ${{ steps.web-ext-build.outputs.target }} 47 | sourceFile: ${{ steps.compress-src.outputs.zip_path }} 48 | manifestFile: dist/manifest.json 49 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # Yarn 4 | /.yarn/* 5 | !/.yarn/patches 6 | !/.yarn/plugins 7 | !/.yarn/releases 8 | !/.yarn/sdks 9 | yarn-error.log 10 | 11 | # compiled output 12 | /dist 13 | /tmp 14 | /out-tsc 15 | # Only exists if Bazel was run 16 | /bazel-out 17 | 18 | # dependencies 19 | /node_modules 20 | 21 | # profiling files 22 | chrome-profiler-events*.json 23 | 24 | # IDEs and editors 25 | .project 26 | .classpath 27 | .c9/ 28 | *.launch 29 | .settings/ 30 | *.sublime-workspace 31 | 32 | # IDE - VSCode 33 | .vscode/* 34 | !.vscode/settings.json 35 | !.vscode/tasks.json 36 | !.vscode/launch.json 37 | !.vscode/extensions.json 38 | .history/* 39 | 40 | # misc 41 | /.angular/cache 42 | /.sass-cache 43 | /connect.lock 44 | /coverage 45 | /libpeerconnection.log 46 | npm-debug.log 47 | testem.log 48 | /typings 49 | 50 | # System Files 51 | .DS_Store 52 | Thumbs.db 53 | 54 | .secrets 55 | -------------------------------------------------------------------------------- /.idea/.gitignore: -------------------------------------------------------------------------------- 1 | # 默认忽略的文件 2 | /shelf/ 3 | /workspace.xml 4 | # 基于编辑器的 HTTP 客户端请求 5 | /httpRequests/ 6 | discord.xml 7 | -------------------------------------------------------------------------------- /.idea/codeStyles/Project.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | -------------------------------------------------------------------------------- /.idea/codeStyles/codeStyleConfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /.idea/externalDependencies.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/git_toolbox_prj.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /.idea/jsLibraryMappings.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/obsidian-web-clipper.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /.idea/runConfigurations/watch.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /.idea/runConfigurations/watch_angular.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/runConfigurations/watch_package.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /.idea/watcherTasks.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .yarn 4 | .angular 5 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "tabWidth": 2, 4 | "singleQuote": true, 5 | "semi": false, 6 | "trailingComma": "none" 7 | } 8 | -------------------------------------------------------------------------------- /.yarn/plugins/@yarnpkg/plugin-typescript.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | //prettier-ignore 3 | module.exports = { 4 | name: "@yarnpkg/plugin-typescript", 5 | factory: function (require) { 6 | var plugin=(()=>{var Ft=Object.create,H=Object.defineProperty,Bt=Object.defineProperties,Kt=Object.getOwnPropertyDescriptor,zt=Object.getOwnPropertyDescriptors,Gt=Object.getOwnPropertyNames,Q=Object.getOwnPropertySymbols,$t=Object.getPrototypeOf,ne=Object.prototype.hasOwnProperty,De=Object.prototype.propertyIsEnumerable;var Re=(e,t,r)=>t in e?H(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,u=(e,t)=>{for(var r in t||(t={}))ne.call(t,r)&&Re(e,r,t[r]);if(Q)for(var r of Q(t))De.call(t,r)&&Re(e,r,t[r]);return e},g=(e,t)=>Bt(e,zt(t)),Lt=e=>H(e,"__esModule",{value:!0});var R=(e,t)=>{var r={};for(var s in e)ne.call(e,s)&&t.indexOf(s)<0&&(r[s]=e[s]);if(e!=null&&Q)for(var s of Q(e))t.indexOf(s)<0&&De.call(e,s)&&(r[s]=e[s]);return r};var I=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),Vt=(e,t)=>{for(var r in t)H(e,r,{get:t[r],enumerable:!0})},Qt=(e,t,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let s of Gt(t))!ne.call(e,s)&&s!=="default"&&H(e,s,{get:()=>t[s],enumerable:!(r=Kt(t,s))||r.enumerable});return e},C=e=>Qt(Lt(H(e!=null?Ft($t(e)):{},"default",e&&e.__esModule&&"default"in e?{get:()=>e.default,enumerable:!0}:{value:e,enumerable:!0})),e);var xe=I(J=>{"use strict";Object.defineProperty(J,"__esModule",{value:!0});function _(e){let t=[...e.caches],r=t.shift();return r===void 0?ve():{get(s,n,a={miss:()=>Promise.resolve()}){return r.get(s,n,a).catch(()=>_({caches:t}).get(s,n,a))},set(s,n){return r.set(s,n).catch(()=>_({caches:t}).set(s,n))},delete(s){return r.delete(s).catch(()=>_({caches:t}).delete(s))},clear(){return r.clear().catch(()=>_({caches:t}).clear())}}}function ve(){return{get(e,t,r={miss:()=>Promise.resolve()}){return t().then(n=>Promise.all([n,r.miss(n)])).then(([n])=>n)},set(e,t){return Promise.resolve(t)},delete(e){return Promise.resolve()},clear(){return Promise.resolve()}}}J.createFallbackableCache=_;J.createNullCache=ve});var Ee=I(($s,qe)=>{qe.exports=xe()});var Te=I(ae=>{"use strict";Object.defineProperty(ae,"__esModule",{value:!0});function Jt(e={serializable:!0}){let t={};return{get(r,s,n={miss:()=>Promise.resolve()}){let a=JSON.stringify(r);if(a in t)return Promise.resolve(e.serializable?JSON.parse(t[a]):t[a]);let o=s(),d=n&&n.miss||(()=>Promise.resolve());return o.then(y=>d(y)).then(()=>o)},set(r,s){return t[JSON.stringify(r)]=e.serializable?JSON.stringify(s):s,Promise.resolve(s)},delete(r){return delete t[JSON.stringify(r)],Promise.resolve()},clear(){return t={},Promise.resolve()}}}ae.createInMemoryCache=Jt});var we=I((Vs,Me)=>{Me.exports=Te()});var Ce=I(M=>{"use strict";Object.defineProperty(M,"__esModule",{value:!0});function Xt(e,t,r){let s={"x-algolia-api-key":r,"x-algolia-application-id":t};return{headers(){return e===oe.WithinHeaders?s:{}},queryParameters(){return e===oe.WithinQueryParameters?s:{}}}}function Yt(e){let t=0,r=()=>(t++,new Promise(s=>{setTimeout(()=>{s(e(r))},Math.min(100*t,1e3))}));return e(r)}function ke(e,t=(r,s)=>Promise.resolve()){return Object.assign(e,{wait(r){return ke(e.then(s=>Promise.all([t(s,r),s])).then(s=>s[1]))}})}function Zt(e){let t=e.length-1;for(t;t>0;t--){let r=Math.floor(Math.random()*(t+1)),s=e[t];e[t]=e[r],e[r]=s}return e}function er(e,t){return Object.keys(t!==void 0?t:{}).forEach(r=>{e[r]=t[r](e)}),e}function tr(e,...t){let r=0;return e.replace(/%s/g,()=>encodeURIComponent(t[r++]))}var rr="4.2.0",sr=e=>()=>e.transporter.requester.destroy(),oe={WithinQueryParameters:0,WithinHeaders:1};M.AuthMode=oe;M.addMethods=er;M.createAuth=Xt;M.createRetryablePromise=Yt;M.createWaitablePromise=ke;M.destroy=sr;M.encode=tr;M.shuffle=Zt;M.version=rr});var F=I((Js,Ue)=>{Ue.exports=Ce()});var Ne=I(ie=>{"use strict";Object.defineProperty(ie,"__esModule",{value:!0});var nr={Delete:"DELETE",Get:"GET",Post:"POST",Put:"PUT"};ie.MethodEnum=nr});var B=I((Ys,We)=>{We.exports=Ne()});var Ze=I(A=>{"use strict";Object.defineProperty(A,"__esModule",{value:!0});var He=B();function ce(e,t){let r=e||{},s=r.data||{};return Object.keys(r).forEach(n=>{["timeout","headers","queryParameters","data","cacheable"].indexOf(n)===-1&&(s[n]=r[n])}),{data:Object.entries(s).length>0?s:void 0,timeout:r.timeout||t,headers:r.headers||{},queryParameters:r.queryParameters||{},cacheable:r.cacheable}}var X={Read:1,Write:2,Any:3},U={Up:1,Down:2,Timeouted:3},_e=2*60*1e3;function ue(e,t=U.Up){return g(u({},e),{status:t,lastUpdate:Date.now()})}function Fe(e){return e.status===U.Up||Date.now()-e.lastUpdate>_e}function Be(e){return e.status===U.Timeouted&&Date.now()-e.lastUpdate<=_e}function le(e){return{protocol:e.protocol||"https",url:e.url,accept:e.accept||X.Any}}function ar(e,t){return Promise.all(t.map(r=>e.get(r,()=>Promise.resolve(ue(r))))).then(r=>{let s=r.filter(d=>Fe(d)),n=r.filter(d=>Be(d)),a=[...s,...n],o=a.length>0?a.map(d=>le(d)):t;return{getTimeout(d,y){return(n.length===0&&d===0?1:n.length+3+d)*y},statelessHosts:o}})}var or=({isTimedOut:e,status:t})=>!e&&~~t==0,ir=e=>{let t=e.status;return e.isTimedOut||or(e)||~~(t/100)!=2&&~~(t/100)!=4},cr=({status:e})=>~~(e/100)==2,ur=(e,t)=>ir(e)?t.onRetry(e):cr(e)?t.onSucess(e):t.onFail(e);function Qe(e,t,r,s){let n=[],a=$e(r,s),o=Le(e,s),d=r.method,y=r.method!==He.MethodEnum.Get?{}:u(u({},r.data),s.data),b=u(u(u({"x-algolia-agent":e.userAgent.value},e.queryParameters),y),s.queryParameters),f=0,p=(h,S)=>{let O=h.pop();if(O===void 0)throw Ve(de(n));let P={data:a,headers:o,method:d,url:Ge(O,r.path,b),connectTimeout:S(f,e.timeouts.connect),responseTimeout:S(f,s.timeout)},x=j=>{let T={request:P,response:j,host:O,triesLeft:h.length};return n.push(T),T},v={onSucess:j=>Ke(j),onRetry(j){let T=x(j);return j.isTimedOut&&f++,Promise.all([e.logger.info("Retryable failure",pe(T)),e.hostsCache.set(O,ue(O,j.isTimedOut?U.Timeouted:U.Down))]).then(()=>p(h,S))},onFail(j){throw x(j),ze(j,de(n))}};return e.requester.send(P).then(j=>ur(j,v))};return ar(e.hostsCache,t).then(h=>p([...h.statelessHosts].reverse(),h.getTimeout))}function lr(e){let{hostsCache:t,logger:r,requester:s,requestsCache:n,responsesCache:a,timeouts:o,userAgent:d,hosts:y,queryParameters:b,headers:f}=e,p={hostsCache:t,logger:r,requester:s,requestsCache:n,responsesCache:a,timeouts:o,userAgent:d,headers:f,queryParameters:b,hosts:y.map(h=>le(h)),read(h,S){let O=ce(S,p.timeouts.read),P=()=>Qe(p,p.hosts.filter(j=>(j.accept&X.Read)!=0),h,O);if((O.cacheable!==void 0?O.cacheable:h.cacheable)!==!0)return P();let v={request:h,mappedRequestOptions:O,transporter:{queryParameters:p.queryParameters,headers:p.headers}};return p.responsesCache.get(v,()=>p.requestsCache.get(v,()=>p.requestsCache.set(v,P()).then(j=>Promise.all([p.requestsCache.delete(v),j]),j=>Promise.all([p.requestsCache.delete(v),Promise.reject(j)])).then(([j,T])=>T)),{miss:j=>p.responsesCache.set(v,j)})},write(h,S){return Qe(p,p.hosts.filter(O=>(O.accept&X.Write)!=0),h,ce(S,p.timeouts.write))}};return p}function dr(e){let t={value:`Algolia for JavaScript (${e})`,add(r){let s=`; ${r.segment}${r.version!==void 0?` (${r.version})`:""}`;return t.value.indexOf(s)===-1&&(t.value=`${t.value}${s}`),t}};return t}function Ke(e){try{return JSON.parse(e.content)}catch(t){throw Je(t.message,e)}}function ze({content:e,status:t},r){let s=e;try{s=JSON.parse(e).message}catch(n){}return Xe(s,t,r)}function pr(e,...t){let r=0;return e.replace(/%s/g,()=>encodeURIComponent(t[r++]))}function Ge(e,t,r){let s=Ye(r),n=`${e.protocol}://${e.url}/${t.charAt(0)==="/"?t.substr(1):t}`;return s.length&&(n+=`?${s}`),n}function Ye(e){let t=r=>Object.prototype.toString.call(r)==="[object Object]"||Object.prototype.toString.call(r)==="[object Array]";return Object.keys(e).map(r=>pr("%s=%s",r,t(e[r])?JSON.stringify(e[r]):e[r])).join("&")}function $e(e,t){if(e.method===He.MethodEnum.Get||e.data===void 0&&t.data===void 0)return;let r=Array.isArray(e.data)?e.data:u(u({},e.data),t.data);return JSON.stringify(r)}function Le(e,t){let r=u(u({},e.headers),t.headers),s={};return Object.keys(r).forEach(n=>{let a=r[n];s[n.toLowerCase()]=a}),s}function de(e){return e.map(t=>pe(t))}function pe(e){let t=e.request.headers["x-algolia-api-key"]?{"x-algolia-api-key":"*****"}:{};return g(u({},e),{request:g(u({},e.request),{headers:u(u({},e.request.headers),t)})})}function Xe(e,t,r){return{name:"ApiError",message:e,status:t,transporterStackTrace:r}}function Je(e,t){return{name:"DeserializationError",message:e,response:t}}function Ve(e){return{name:"RetryError",message:"Unreachable hosts - your application id may be incorrect. If the error persists, contact support@algolia.com.",transporterStackTrace:e}}A.CallEnum=X;A.HostStatusEnum=U;A.createApiError=Xe;A.createDeserializationError=Je;A.createMappedRequestOptions=ce;A.createRetryError=Ve;A.createStatefulHost=ue;A.createStatelessHost=le;A.createTransporter=lr;A.createUserAgent=dr;A.deserializeFailure=ze;A.deserializeSuccess=Ke;A.isStatefulHostTimeouted=Be;A.isStatefulHostUp=Fe;A.serializeData=$e;A.serializeHeaders=Le;A.serializeQueryParameters=Ye;A.serializeUrl=Ge;A.stackFrameWithoutCredentials=pe;A.stackTraceWithoutCredentials=de});var K=I((en,et)=>{et.exports=Ze()});var tt=I(w=>{"use strict";Object.defineProperty(w,"__esModule",{value:!0});var N=F(),mr=K(),z=B(),hr=e=>{let t=e.region||"us",r=N.createAuth(N.AuthMode.WithinHeaders,e.appId,e.apiKey),s=mr.createTransporter(g(u({hosts:[{url:`analytics.${t}.algolia.com`}]},e),{headers:u(g(u({},r.headers()),{"content-type":"application/json"}),e.headers),queryParameters:u(u({},r.queryParameters()),e.queryParameters)})),n=e.appId;return N.addMethods({appId:n,transporter:s},e.methods)},yr=e=>(t,r)=>e.transporter.write({method:z.MethodEnum.Post,path:"2/abtests",data:t},r),gr=e=>(t,r)=>e.transporter.write({method:z.MethodEnum.Delete,path:N.encode("2/abtests/%s",t)},r),fr=e=>(t,r)=>e.transporter.read({method:z.MethodEnum.Get,path:N.encode("2/abtests/%s",t)},r),br=e=>t=>e.transporter.read({method:z.MethodEnum.Get,path:"2/abtests"},t),Pr=e=>(t,r)=>e.transporter.write({method:z.MethodEnum.Post,path:N.encode("2/abtests/%s/stop",t)},r);w.addABTest=yr;w.createAnalyticsClient=hr;w.deleteABTest=gr;w.getABTest=fr;w.getABTests=br;w.stopABTest=Pr});var st=I((rn,rt)=>{rt.exports=tt()});var at=I(G=>{"use strict";Object.defineProperty(G,"__esModule",{value:!0});var me=F(),jr=K(),nt=B(),Or=e=>{let t=e.region||"us",r=me.createAuth(me.AuthMode.WithinHeaders,e.appId,e.apiKey),s=jr.createTransporter(g(u({hosts:[{url:`recommendation.${t}.algolia.com`}]},e),{headers:u(g(u({},r.headers()),{"content-type":"application/json"}),e.headers),queryParameters:u(u({},r.queryParameters()),e.queryParameters)}));return me.addMethods({appId:e.appId,transporter:s},e.methods)},Ir=e=>t=>e.transporter.read({method:nt.MethodEnum.Get,path:"1/strategies/personalization"},t),Ar=e=>(t,r)=>e.transporter.write({method:nt.MethodEnum.Post,path:"1/strategies/personalization",data:t},r);G.createRecommendationClient=Or;G.getPersonalizationStrategy=Ir;G.setPersonalizationStrategy=Ar});var it=I((nn,ot)=>{ot.exports=at()});var jt=I(i=>{"use strict";Object.defineProperty(i,"__esModule",{value:!0});var l=F(),q=K(),m=B(),Sr=require("crypto");function Y(e){let t=r=>e.request(r).then(s=>{if(e.batch!==void 0&&e.batch(s.hits),!e.shouldStop(s))return s.cursor?t({cursor:s.cursor}):t({page:(r.page||0)+1})});return t({})}var Dr=e=>{let t=e.appId,r=l.createAuth(e.authMode!==void 0?e.authMode:l.AuthMode.WithinHeaders,t,e.apiKey),s=q.createTransporter(g(u({hosts:[{url:`${t}-dsn.algolia.net`,accept:q.CallEnum.Read},{url:`${t}.algolia.net`,accept:q.CallEnum.Write}].concat(l.shuffle([{url:`${t}-1.algolianet.com`},{url:`${t}-2.algolianet.com`},{url:`${t}-3.algolianet.com`}]))},e),{headers:u(g(u({},r.headers()),{"content-type":"application/x-www-form-urlencoded"}),e.headers),queryParameters:u(u({},r.queryParameters()),e.queryParameters)})),n={transporter:s,appId:t,addAlgoliaAgent(a,o){s.userAgent.add({segment:a,version:o})},clearCache(){return Promise.all([s.requestsCache.clear(),s.responsesCache.clear()]).then(()=>{})}};return l.addMethods(n,e.methods)};function ct(){return{name:"MissingObjectIDError",message:"All objects must have an unique objectID (like a primary key) to be valid. Algolia is also able to generate objectIDs automatically but *it's not recommended*. To do it, use the `{'autoGenerateObjectIDIfNotExist': true}` option."}}function ut(){return{name:"ObjectNotFoundError",message:"Object not found."}}function lt(){return{name:"ValidUntilNotFoundError",message:"ValidUntil not found in given secured api key."}}var Rr=e=>(t,r)=>{let d=r||{},{queryParameters:s}=d,n=R(d,["queryParameters"]),a=u({acl:t},s!==void 0?{queryParameters:s}:{}),o=(y,b)=>l.createRetryablePromise(f=>$(e)(y.key,b).catch(p=>{if(p.status!==404)throw p;return f()}));return l.createWaitablePromise(e.transporter.write({method:m.MethodEnum.Post,path:"1/keys",data:a},n),o)},vr=e=>(t,r,s)=>{let n=q.createMappedRequestOptions(s);return n.queryParameters["X-Algolia-User-ID"]=t,e.transporter.write({method:m.MethodEnum.Post,path:"1/clusters/mapping",data:{cluster:r}},n)},xr=e=>(t,r,s)=>e.transporter.write({method:m.MethodEnum.Post,path:"1/clusters/mapping/batch",data:{users:t,cluster:r}},s),Z=e=>(t,r,s)=>{let n=(a,o)=>L(e)(t,{methods:{waitTask:D}}).waitTask(a.taskID,o);return l.createWaitablePromise(e.transporter.write({method:m.MethodEnum.Post,path:l.encode("1/indexes/%s/operation",t),data:{operation:"copy",destination:r}},s),n)},qr=e=>(t,r,s)=>Z(e)(t,r,g(u({},s),{scope:[ee.Rules]})),Er=e=>(t,r,s)=>Z(e)(t,r,g(u({},s),{scope:[ee.Settings]})),Tr=e=>(t,r,s)=>Z(e)(t,r,g(u({},s),{scope:[ee.Synonyms]})),Mr=e=>(t,r)=>{let s=(n,a)=>l.createRetryablePromise(o=>$(e)(t,a).then(o).catch(d=>{if(d.status!==404)throw d}));return l.createWaitablePromise(e.transporter.write({method:m.MethodEnum.Delete,path:l.encode("1/keys/%s",t)},r),s)},wr=()=>(e,t)=>{let r=q.serializeQueryParameters(t),s=Sr.createHmac("sha256",e).update(r).digest("hex");return Buffer.from(s+r).toString("base64")},$=e=>(t,r)=>e.transporter.read({method:m.MethodEnum.Get,path:l.encode("1/keys/%s",t)},r),kr=e=>t=>e.transporter.read({method:m.MethodEnum.Get,path:"1/logs"},t),Cr=()=>e=>{let t=Buffer.from(e,"base64").toString("ascii"),r=/validUntil=(\d+)/,s=t.match(r);if(s===null)throw lt();return parseInt(s[1],10)-Math.round(new Date().getTime()/1e3)},Ur=e=>t=>e.transporter.read({method:m.MethodEnum.Get,path:"1/clusters/mapping/top"},t),Nr=e=>(t,r)=>e.transporter.read({method:m.MethodEnum.Get,path:l.encode("1/clusters/mapping/%s",t)},r),Wr=e=>t=>{let n=t||{},{retrieveMappings:r}=n,s=R(n,["retrieveMappings"]);return r===!0&&(s.getClusters=!0),e.transporter.read({method:m.MethodEnum.Get,path:"1/clusters/mapping/pending"},s)},L=e=>(t,r={})=>{let s={transporter:e.transporter,appId:e.appId,indexName:t};return l.addMethods(s,r.methods)},Hr=e=>t=>e.transporter.read({method:m.MethodEnum.Get,path:"1/keys"},t),_r=e=>t=>e.transporter.read({method:m.MethodEnum.Get,path:"1/clusters"},t),Fr=e=>t=>e.transporter.read({method:m.MethodEnum.Get,path:"1/indexes"},t),Br=e=>t=>e.transporter.read({method:m.MethodEnum.Get,path:"1/clusters/mapping"},t),Kr=e=>(t,r,s)=>{let n=(a,o)=>L(e)(t,{methods:{waitTask:D}}).waitTask(a.taskID,o);return l.createWaitablePromise(e.transporter.write({method:m.MethodEnum.Post,path:l.encode("1/indexes/%s/operation",t),data:{operation:"move",destination:r}},s),n)},zr=e=>(t,r)=>{let s=(n,a)=>Promise.all(Object.keys(n.taskID).map(o=>L(e)(o,{methods:{waitTask:D}}).waitTask(n.taskID[o],a)));return l.createWaitablePromise(e.transporter.write({method:m.MethodEnum.Post,path:"1/indexes/*/batch",data:{requests:t}},r),s)},Gr=e=>(t,r)=>e.transporter.read({method:m.MethodEnum.Post,path:"1/indexes/*/objects",data:{requests:t}},r),$r=e=>(t,r)=>{let s=t.map(n=>g(u({},n),{params:q.serializeQueryParameters(n.params||{})}));return e.transporter.read({method:m.MethodEnum.Post,path:"1/indexes/*/queries",data:{requests:s},cacheable:!0},r)},Lr=e=>(t,r)=>Promise.all(t.map(s=>{let d=s.params,{facetName:n,facetQuery:a}=d,o=R(d,["facetName","facetQuery"]);return L(e)(s.indexName,{methods:{searchForFacetValues:dt}}).searchForFacetValues(n,a,u(u({},r),o))})),Vr=e=>(t,r)=>{let s=q.createMappedRequestOptions(r);return s.queryParameters["X-Algolia-User-ID"]=t,e.transporter.write({method:m.MethodEnum.Delete,path:"1/clusters/mapping"},s)},Qr=e=>(t,r)=>{let s=(n,a)=>l.createRetryablePromise(o=>$(e)(t,a).catch(d=>{if(d.status!==404)throw d;return o()}));return l.createWaitablePromise(e.transporter.write({method:m.MethodEnum.Post,path:l.encode("1/keys/%s/restore",t)},r),s)},Jr=e=>(t,r)=>e.transporter.read({method:m.MethodEnum.Post,path:"1/clusters/mapping/search",data:{query:t}},r),Xr=e=>(t,r)=>{let s=Object.assign({},r),f=r||{},{queryParameters:n}=f,a=R(f,["queryParameters"]),o=n?{queryParameters:n}:{},d=["acl","indexes","referers","restrictSources","queryParameters","description","maxQueriesPerIPPerHour","maxHitsPerQuery"],y=p=>Object.keys(s).filter(h=>d.indexOf(h)!==-1).every(h=>p[h]===s[h]),b=(p,h)=>l.createRetryablePromise(S=>$(e)(t,h).then(O=>y(O)?Promise.resolve():S()));return l.createWaitablePromise(e.transporter.write({method:m.MethodEnum.Put,path:l.encode("1/keys/%s",t),data:o},a),b)},pt=e=>(t,r)=>{let s=(n,a)=>D(e)(n.taskID,a);return l.createWaitablePromise(e.transporter.write({method:m.MethodEnum.Post,path:l.encode("1/indexes/%s/batch",e.indexName),data:{requests:t}},r),s)},Yr=e=>t=>Y(g(u({},t),{shouldStop:r=>r.cursor===void 0,request:r=>e.transporter.read({method:m.MethodEnum.Post,path:l.encode("1/indexes/%s/browse",e.indexName),data:r},t)})),Zr=e=>t=>{let r=u({hitsPerPage:1e3},t);return Y(g(u({},r),{shouldStop:s=>s.hits.lengthg(u({},n),{hits:n.hits.map(a=>(delete a._highlightResult,a))}))}}))},es=e=>t=>{let r=u({hitsPerPage:1e3},t);return Y(g(u({},r),{shouldStop:s=>s.hits.lengthg(u({},n),{hits:n.hits.map(a=>(delete a._highlightResult,a))}))}}))},te=e=>(t,r,s)=>{let y=s||{},{batchSize:n}=y,a=R(y,["batchSize"]),o={taskIDs:[],objectIDs:[]},d=(b=0)=>{let f=[],p;for(p=b;p({action:r,body:h})),a).then(h=>(o.objectIDs=o.objectIDs.concat(h.objectIDs),o.taskIDs.push(h.taskID),p++,d(p)))};return l.createWaitablePromise(d(),(b,f)=>Promise.all(b.taskIDs.map(p=>D(e)(p,f))))},ts=e=>t=>l.createWaitablePromise(e.transporter.write({method:m.MethodEnum.Post,path:l.encode("1/indexes/%s/clear",e.indexName)},t),(r,s)=>D(e)(r.taskID,s)),rs=e=>t=>{let a=t||{},{forwardToReplicas:r}=a,s=R(a,["forwardToReplicas"]),n=q.createMappedRequestOptions(s);return r&&(n.queryParameters.forwardToReplicas=1),l.createWaitablePromise(e.transporter.write({method:m.MethodEnum.Post,path:l.encode("1/indexes/%s/rules/clear",e.indexName)},n),(o,d)=>D(e)(o.taskID,d))},ss=e=>t=>{let a=t||{},{forwardToReplicas:r}=a,s=R(a,["forwardToReplicas"]),n=q.createMappedRequestOptions(s);return r&&(n.queryParameters.forwardToReplicas=1),l.createWaitablePromise(e.transporter.write({method:m.MethodEnum.Post,path:l.encode("1/indexes/%s/synonyms/clear",e.indexName)},n),(o,d)=>D(e)(o.taskID,d))},ns=e=>(t,r)=>l.createWaitablePromise(e.transporter.write({method:m.MethodEnum.Post,path:l.encode("1/indexes/%s/deleteByQuery",e.indexName),data:t},r),(s,n)=>D(e)(s.taskID,n)),as=e=>t=>l.createWaitablePromise(e.transporter.write({method:m.MethodEnum.Delete,path:l.encode("1/indexes/%s",e.indexName)},t),(r,s)=>D(e)(r.taskID,s)),os=e=>(t,r)=>l.createWaitablePromise(yt(e)([t],r).then(s=>({taskID:s.taskIDs[0]})),(s,n)=>D(e)(s.taskID,n)),yt=e=>(t,r)=>{let s=t.map(n=>({objectID:n}));return te(e)(s,k.DeleteObject,r)},is=e=>(t,r)=>{let o=r||{},{forwardToReplicas:s}=o,n=R(o,["forwardToReplicas"]),a=q.createMappedRequestOptions(n);return s&&(a.queryParameters.forwardToReplicas=1),l.createWaitablePromise(e.transporter.write({method:m.MethodEnum.Delete,path:l.encode("1/indexes/%s/rules/%s",e.indexName,t)},a),(d,y)=>D(e)(d.taskID,y))},cs=e=>(t,r)=>{let o=r||{},{forwardToReplicas:s}=o,n=R(o,["forwardToReplicas"]),a=q.createMappedRequestOptions(n);return s&&(a.queryParameters.forwardToReplicas=1),l.createWaitablePromise(e.transporter.write({method:m.MethodEnum.Delete,path:l.encode("1/indexes/%s/synonyms/%s",e.indexName,t)},a),(d,y)=>D(e)(d.taskID,y))},us=e=>t=>gt(e)(t).then(()=>!0).catch(r=>{if(r.status!==404)throw r;return!1}),ls=e=>(t,r)=>{let y=r||{},{query:s,paginate:n}=y,a=R(y,["query","paginate"]),o=0,d=()=>ft(e)(s||"",g(u({},a),{page:o})).then(b=>{for(let[f,p]of Object.entries(b.hits))if(t(p))return{object:p,position:parseInt(f,10),page:o};if(o++,n===!1||o>=b.nbPages)throw ut();return d()});return d()},ds=e=>(t,r)=>e.transporter.read({method:m.MethodEnum.Get,path:l.encode("1/indexes/%s/%s",e.indexName,t)},r),ps=()=>(e,t)=>{for(let[r,s]of Object.entries(e.hits))if(s.objectID===t)return parseInt(r,10);return-1},ms=e=>(t,r)=>{let o=r||{},{attributesToRetrieve:s}=o,n=R(o,["attributesToRetrieve"]),a=t.map(d=>u({indexName:e.indexName,objectID:d},s?{attributesToRetrieve:s}:{}));return e.transporter.read({method:m.MethodEnum.Post,path:"1/indexes/*/objects",data:{requests:a}},n)},hs=e=>(t,r)=>e.transporter.read({method:m.MethodEnum.Get,path:l.encode("1/indexes/%s/rules/%s",e.indexName,t)},r),gt=e=>t=>e.transporter.read({method:m.MethodEnum.Get,path:l.encode("1/indexes/%s/settings",e.indexName),data:{getVersion:2}},t),ys=e=>(t,r)=>e.transporter.read({method:m.MethodEnum.Get,path:l.encode("1/indexes/%s/synonyms/%s",e.indexName,t)},r),bt=e=>(t,r)=>e.transporter.read({method:m.MethodEnum.Get,path:l.encode("1/indexes/%s/task/%s",e.indexName,t.toString())},r),gs=e=>(t,r)=>l.createWaitablePromise(Pt(e)([t],r).then(s=>({objectID:s.objectIDs[0],taskID:s.taskIDs[0]})),(s,n)=>D(e)(s.taskID,n)),Pt=e=>(t,r)=>{let o=r||{},{createIfNotExists:s}=o,n=R(o,["createIfNotExists"]),a=s?k.PartialUpdateObject:k.PartialUpdateObjectNoCreate;return te(e)(t,a,n)},fs=e=>(t,r)=>{let O=r||{},{safe:s,autoGenerateObjectIDIfNotExist:n,batchSize:a}=O,o=R(O,["safe","autoGenerateObjectIDIfNotExist","batchSize"]),d=(P,x,v,j)=>l.createWaitablePromise(e.transporter.write({method:m.MethodEnum.Post,path:l.encode("1/indexes/%s/operation",P),data:{operation:v,destination:x}},j),(T,V)=>D(e)(T.taskID,V)),y=Math.random().toString(36).substring(7),b=`${e.indexName}_tmp_${y}`,f=he({appId:e.appId,transporter:e.transporter,indexName:b}),p=[],h=d(e.indexName,b,"copy",g(u({},o),{scope:["settings","synonyms","rules"]}));p.push(h);let S=(s?h.wait(o):h).then(()=>{let P=f(t,g(u({},o),{autoGenerateObjectIDIfNotExist:n,batchSize:a}));return p.push(P),s?P.wait(o):P}).then(()=>{let P=d(b,e.indexName,"move",o);return p.push(P),s?P.wait(o):P}).then(()=>Promise.all(p)).then(([P,x,v])=>({objectIDs:x.objectIDs,taskIDs:[P.taskID,...x.taskIDs,v.taskID]}));return l.createWaitablePromise(S,(P,x)=>Promise.all(p.map(v=>v.wait(x))))},bs=e=>(t,r)=>ye(e)(t,g(u({},r),{clearExistingRules:!0})),Ps=e=>(t,r)=>ge(e)(t,g(u({},r),{replaceExistingSynonyms:!0})),js=e=>(t,r)=>l.createWaitablePromise(he(e)([t],r).then(s=>({objectID:s.objectIDs[0],taskID:s.taskIDs[0]})),(s,n)=>D(e)(s.taskID,n)),he=e=>(t,r)=>{let o=r||{},{autoGenerateObjectIDIfNotExist:s}=o,n=R(o,["autoGenerateObjectIDIfNotExist"]),a=s?k.AddObject:k.UpdateObject;if(a===k.UpdateObject){for(let d of t)if(d.objectID===void 0)return l.createWaitablePromise(Promise.reject(ct()))}return te(e)(t,a,n)},Os=e=>(t,r)=>ye(e)([t],r),ye=e=>(t,r)=>{let d=r||{},{forwardToReplicas:s,clearExistingRules:n}=d,a=R(d,["forwardToReplicas","clearExistingRules"]),o=q.createMappedRequestOptions(a);return s&&(o.queryParameters.forwardToReplicas=1),n&&(o.queryParameters.clearExistingRules=1),l.createWaitablePromise(e.transporter.write({method:m.MethodEnum.Post,path:l.encode("1/indexes/%s/rules/batch",e.indexName),data:t},o),(y,b)=>D(e)(y.taskID,b))},Is=e=>(t,r)=>ge(e)([t],r),ge=e=>(t,r)=>{let d=r||{},{forwardToReplicas:s,replaceExistingSynonyms:n}=d,a=R(d,["forwardToReplicas","replaceExistingSynonyms"]),o=q.createMappedRequestOptions(a);return s&&(o.queryParameters.forwardToReplicas=1),n&&(o.queryParameters.replaceExistingSynonyms=1),l.createWaitablePromise(e.transporter.write({method:m.MethodEnum.Post,path:l.encode("1/indexes/%s/synonyms/batch",e.indexName),data:t},o),(y,b)=>D(e)(y.taskID,b))},ft=e=>(t,r)=>e.transporter.read({method:m.MethodEnum.Post,path:l.encode("1/indexes/%s/query",e.indexName),data:{query:t},cacheable:!0},r),dt=e=>(t,r,s)=>e.transporter.read({method:m.MethodEnum.Post,path:l.encode("1/indexes/%s/facets/%s/query",e.indexName,t),data:{facetQuery:r},cacheable:!0},s),mt=e=>(t,r)=>e.transporter.read({method:m.MethodEnum.Post,path:l.encode("1/indexes/%s/rules/search",e.indexName),data:{query:t}},r),ht=e=>(t,r)=>e.transporter.read({method:m.MethodEnum.Post,path:l.encode("1/indexes/%s/synonyms/search",e.indexName),data:{query:t}},r),As=e=>(t,r)=>{let o=r||{},{forwardToReplicas:s}=o,n=R(o,["forwardToReplicas"]),a=q.createMappedRequestOptions(n);return s&&(a.queryParameters.forwardToReplicas=1),l.createWaitablePromise(e.transporter.write({method:m.MethodEnum.Put,path:l.encode("1/indexes/%s/settings",e.indexName),data:t},a),(d,y)=>D(e)(d.taskID,y))},D=e=>(t,r)=>l.createRetryablePromise(s=>bt(e)(t,r).then(n=>n.status!=="published"?s():void 0)),Ss={AddObject:"addObject",Analytics:"analytics",Browser:"browse",DeleteIndex:"deleteIndex",DeleteObject:"deleteObject",EditSettings:"editSettings",ListIndexes:"listIndexes",Logs:"logs",Recommendation:"recommendation",Search:"search",SeeUnretrievableAttributes:"seeUnretrievableAttributes",Settings:"settings",Usage:"usage"},k={AddObject:"addObject",UpdateObject:"updateObject",PartialUpdateObject:"partialUpdateObject",PartialUpdateObjectNoCreate:"partialUpdateObjectNoCreate",DeleteObject:"deleteObject"},ee={Settings:"settings",Synonyms:"synonyms",Rules:"rules"},Ds={None:"none",StopIfEnoughMatches:"stopIfEnoughMatches"},Rs={Synonym:"synonym",OneWaySynonym:"oneWaySynonym",AltCorrection1:"altCorrection1",AltCorrection2:"altCorrection2",Placeholder:"placeholder"};i.ApiKeyACLEnum=Ss;i.BatchActionEnum=k;i.ScopeEnum=ee;i.StrategyEnum=Ds;i.SynonymEnum=Rs;i.addApiKey=Rr;i.assignUserID=vr;i.assignUserIDs=xr;i.batch=pt;i.browseObjects=Yr;i.browseRules=Zr;i.browseSynonyms=es;i.chunkedBatch=te;i.clearObjects=ts;i.clearRules=rs;i.clearSynonyms=ss;i.copyIndex=Z;i.copyRules=qr;i.copySettings=Er;i.copySynonyms=Tr;i.createBrowsablePromise=Y;i.createMissingObjectIDError=ct;i.createObjectNotFoundError=ut;i.createSearchClient=Dr;i.createValidUntilNotFoundError=lt;i.deleteApiKey=Mr;i.deleteBy=ns;i.deleteIndex=as;i.deleteObject=os;i.deleteObjects=yt;i.deleteRule=is;i.deleteSynonym=cs;i.exists=us;i.findObject=ls;i.generateSecuredApiKey=wr;i.getApiKey=$;i.getLogs=kr;i.getObject=ds;i.getObjectPosition=ps;i.getObjects=ms;i.getRule=hs;i.getSecuredApiKeyRemainingValidity=Cr;i.getSettings=gt;i.getSynonym=ys;i.getTask=bt;i.getTopUserIDs=Ur;i.getUserID=Nr;i.hasPendingMappings=Wr;i.initIndex=L;i.listApiKeys=Hr;i.listClusters=_r;i.listIndices=Fr;i.listUserIDs=Br;i.moveIndex=Kr;i.multipleBatch=zr;i.multipleGetObjects=Gr;i.multipleQueries=$r;i.multipleSearchForFacetValues=Lr;i.partialUpdateObject=gs;i.partialUpdateObjects=Pt;i.removeUserID=Vr;i.replaceAllObjects=fs;i.replaceAllRules=bs;i.replaceAllSynonyms=Ps;i.restoreApiKey=Qr;i.saveObject=js;i.saveObjects=he;i.saveRule=Os;i.saveRules=ye;i.saveSynonym=Is;i.saveSynonyms=ge;i.search=ft;i.searchForFacetValues=dt;i.searchRules=mt;i.searchSynonyms=ht;i.searchUserIDs=Jr;i.setSettings=As;i.updateApiKey=Xr;i.waitTask=D});var It=I((on,Ot)=>{Ot.exports=jt()});var At=I(re=>{"use strict";Object.defineProperty(re,"__esModule",{value:!0});function vs(){return{debug(e,t){return Promise.resolve()},info(e,t){return Promise.resolve()},error(e,t){return Promise.resolve()}}}var xs={Debug:1,Info:2,Error:3};re.LogLevelEnum=xs;re.createNullLogger=vs});var Dt=I((un,St)=>{St.exports=At()});var xt=I(fe=>{"use strict";Object.defineProperty(fe,"__esModule",{value:!0});var Rt=require("http"),vt=require("https"),qs=require("url");function Es(){let e={keepAlive:!0},t=new Rt.Agent(e),r=new vt.Agent(e);return{send(s){return new Promise(n=>{let a=qs.parse(s.url),o=a.query===null?a.pathname:`${a.pathname}?${a.query}`,d=u({agent:a.protocol==="https:"?r:t,hostname:a.hostname,path:o,method:s.method,headers:s.headers},a.port!==void 0?{port:a.port||""}:{}),y=(a.protocol==="https:"?vt:Rt).request(d,h=>{let S="";h.on("data",O=>S+=O),h.on("end",()=>{clearTimeout(f),clearTimeout(p),n({status:h.statusCode||0,content:S,isTimedOut:!1})})}),b=(h,S)=>setTimeout(()=>{y.abort(),n({status:0,content:S,isTimedOut:!0})},h*1e3),f=b(s.connectTimeout,"Connection timeout"),p;y.on("error",h=>{clearTimeout(f),clearTimeout(p),n({status:0,content:h.message,isTimedOut:!1})}),y.once("response",()=>{clearTimeout(f),p=b(s.responseTimeout,"Socket timeout")}),s.data!==void 0&&y.write(s.data),y.end()})},destroy(){return t.destroy(),r.destroy(),Promise.resolve()}}}fe.createNodeHttpRequester=Es});var Et=I((dn,qt)=>{qt.exports=xt()});var kt=I((pn,Tt)=>{"use strict";var Mt=Ee(),Ts=we(),W=st(),be=F(),Pe=it(),c=It(),Ms=Dt(),ws=Et(),ks=K();function wt(e,t,r){let s={appId:e,apiKey:t,timeouts:{connect:2,read:5,write:30},requester:ws.createNodeHttpRequester(),logger:Ms.createNullLogger(),responsesCache:Mt.createNullCache(),requestsCache:Mt.createNullCache(),hostsCache:Ts.createInMemoryCache(),userAgent:ks.createUserAgent(be.version).add({segment:"Node.js",version:process.versions.node})};return c.createSearchClient(g(u(u({},s),r),{methods:{search:c.multipleQueries,searchForFacetValues:c.multipleSearchForFacetValues,multipleBatch:c.multipleBatch,multipleGetObjects:c.multipleGetObjects,multipleQueries:c.multipleQueries,copyIndex:c.copyIndex,copySettings:c.copySettings,copyRules:c.copyRules,copySynonyms:c.copySynonyms,moveIndex:c.moveIndex,listIndices:c.listIndices,getLogs:c.getLogs,listClusters:c.listClusters,multipleSearchForFacetValues:c.multipleSearchForFacetValues,getApiKey:c.getApiKey,addApiKey:c.addApiKey,listApiKeys:c.listApiKeys,updateApiKey:c.updateApiKey,deleteApiKey:c.deleteApiKey,restoreApiKey:c.restoreApiKey,assignUserID:c.assignUserID,assignUserIDs:c.assignUserIDs,getUserID:c.getUserID,searchUserIDs:c.searchUserIDs,listUserIDs:c.listUserIDs,getTopUserIDs:c.getTopUserIDs,removeUserID:c.removeUserID,hasPendingMappings:c.hasPendingMappings,generateSecuredApiKey:c.generateSecuredApiKey,getSecuredApiKeyRemainingValidity:c.getSecuredApiKeyRemainingValidity,destroy:be.destroy,initIndex:n=>a=>c.initIndex(n)(a,{methods:{batch:c.batch,delete:c.deleteIndex,getObject:c.getObject,getObjects:c.getObjects,saveObject:c.saveObject,saveObjects:c.saveObjects,search:c.search,searchForFacetValues:c.searchForFacetValues,waitTask:c.waitTask,setSettings:c.setSettings,getSettings:c.getSettings,partialUpdateObject:c.partialUpdateObject,partialUpdateObjects:c.partialUpdateObjects,deleteObject:c.deleteObject,deleteObjects:c.deleteObjects,deleteBy:c.deleteBy,clearObjects:c.clearObjects,browseObjects:c.browseObjects,getObjectPosition:c.getObjectPosition,findObject:c.findObject,exists:c.exists,saveSynonym:c.saveSynonym,saveSynonyms:c.saveSynonyms,getSynonym:c.getSynonym,searchSynonyms:c.searchSynonyms,browseSynonyms:c.browseSynonyms,deleteSynonym:c.deleteSynonym,clearSynonyms:c.clearSynonyms,replaceAllObjects:c.replaceAllObjects,replaceAllSynonyms:c.replaceAllSynonyms,searchRules:c.searchRules,getRule:c.getRule,deleteRule:c.deleteRule,saveRule:c.saveRule,saveRules:c.saveRules,replaceAllRules:c.replaceAllRules,browseRules:c.browseRules,clearRules:c.clearRules}}),initAnalytics:()=>n=>W.createAnalyticsClient(g(u(u({},s),n),{methods:{addABTest:W.addABTest,getABTest:W.getABTest,getABTests:W.getABTests,stopABTest:W.stopABTest,deleteABTest:W.deleteABTest}})),initRecommendation:()=>n=>Pe.createRecommendationClient(g(u(u({},s),n),{methods:{getPersonalizationStrategy:Pe.getPersonalizationStrategy,setPersonalizationStrategy:Pe.setPersonalizationStrategy}}))}}))}wt.version=be.version;Tt.exports=wt});var Ut=I((mn,je)=>{var Ct=kt();je.exports=Ct;je.exports.default=Ct});var Ws={};Vt(Ws,{default:()=>Ks});var Oe=C(require("@yarnpkg/core")),E=C(require("@yarnpkg/core")),Ie=C(require("@yarnpkg/plugin-essentials")),Ht=C(require("semver"));var se=C(require("@yarnpkg/core")),Nt=C(Ut()),Cs="e8e1bd300d860104bb8c58453ffa1eb4",Us="OFCNCOG2CU",Wt=async(e,t)=>{var a;let r=se.structUtils.stringifyIdent(e),n=Ns(t).initIndex("npm-search");try{return((a=(await n.getObject(r,{attributesToRetrieve:["types"]})).types)==null?void 0:a.ts)==="definitely-typed"}catch(o){return!1}},Ns=e=>(0,Nt.default)(Us,Cs,{requester:{async send(r){try{let s=await se.httpUtils.request(r.url,r.data||null,{configuration:e,headers:r.headers});return{content:s.body,isTimedOut:!1,status:s.statusCode}}catch(s){return{content:s.response.body,isTimedOut:!1,status:s.response.statusCode}}}}});var _t=e=>e.scope?`${e.scope}__${e.name}`:`${e.name}`,Hs=async(e,t,r,s)=>{if(r.scope==="types")return;let{project:n}=e,{configuration:a}=n,o=a.makeResolver(),d={project:n,resolver:o,report:new E.ThrowReport};if(!await Wt(r,a))return;let b=_t(r),f=E.structUtils.parseRange(r.range).selector;if(!E.semverUtils.validRange(f)){let P=await o.getCandidates(r,new Map,d);f=E.structUtils.parseRange(P[0].reference).selector}let p=Ht.default.coerce(f);if(p===null)return;let h=`${Ie.suggestUtils.Modifier.CARET}${p.major}`,S=E.structUtils.makeDescriptor(E.structUtils.makeIdent("types",b),h),O=E.miscUtils.mapAndFind(n.workspaces,P=>{var T,V;let x=(T=P.manifest.dependencies.get(r.identHash))==null?void 0:T.descriptorHash,v=(V=P.manifest.devDependencies.get(r.identHash))==null?void 0:V.descriptorHash;if(x!==r.descriptorHash&&v!==r.descriptorHash)return E.miscUtils.mapAndFind.skip;let j=[];for(let Ae of Oe.Manifest.allDependencies){let Se=P.manifest[Ae].get(S.identHash);typeof Se!="undefined"&&j.push([Ae,Se])}return j.length===0?E.miscUtils.mapAndFind.skip:j});if(typeof O!="undefined")for(let[P,x]of O)e.manifest[P].set(x.identHash,x);else{try{if((await o.getCandidates(S,new Map,d)).length===0)return}catch{return}e.manifest[Ie.suggestUtils.Target.DEVELOPMENT].set(S.identHash,S)}},_s=async(e,t,r)=>{if(r.scope==="types")return;let s=_t(r),n=E.structUtils.makeIdent("types",s);for(let a of Oe.Manifest.allDependencies)typeof e.manifest[a].get(n.identHash)!="undefined"&&e.manifest[a].delete(n.identHash)},Fs=(e,t)=>{t.publishConfig&&t.publishConfig.typings&&(t.typings=t.publishConfig.typings),t.publishConfig&&t.publishConfig.types&&(t.types=t.publishConfig.types)},Bs={hooks:{afterWorkspaceDependencyAddition:Hs,afterWorkspaceDependencyRemoval:_s,beforeWorkspacePacking:Fs}},Ks=Bs;return Ws;})(); 7 | return plugin; 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | compressionLevel: 0 2 | 3 | nodeLinker: node-modules 4 | 5 | plugins: 6 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 7 | spec: "@yarnpkg/plugin-interactive-tools" 8 | - path: .yarn/plugins/@yarnpkg/plugin-version.cjs 9 | spec: "@yarnpkg/plugin-version" 10 | - path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs 11 | spec: "@yarnpkg/plugin-typescript" 12 | 13 | yarnPath: .yarn/releases/yarn-3.3.0.cjs 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ObsidianWebClipper 2 | 3 | ## Require 4 | ```yaml 5 | node: 16 6 | yarn: * 7 | ``` 8 | 9 | ## Installation 10 | 11 | ```shell 12 | yarn 13 | ``` 14 | 15 | ## Build 16 | 17 | Run `yarn build` to build the project. The built artifacts will be stored in the `dist/` directory. 18 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "cli": { 5 | "packageManager": "yarn", 6 | "schematicCollections": [ 7 | "@angular-eslint/schematics" 8 | ] 9 | }, 10 | "projects": { 11 | "obsidian-web-clipper": { 12 | "projectType": "application", 13 | "schematics": { 14 | "@schematics/angular:component": { 15 | "style": "scss", 16 | "skipTests": true 17 | }, 18 | "@schematics/angular:class": { 19 | "skipTests": true 20 | }, 21 | "@schematics/angular:directive": { 22 | "skipTests": true 23 | }, 24 | "@schematics/angular:guard": { 25 | "skipTests": true 26 | }, 27 | "@schematics/angular:interceptor": { 28 | "skipTests": true 29 | }, 30 | "@schematics/angular:pipe": { 31 | "skipTests": true 32 | }, 33 | "@schematics/angular:service": { 34 | "skipTests": true 35 | }, 36 | "@schematics/angular:application": { 37 | "strict": true 38 | } 39 | }, 40 | "root": "", 41 | "sourceRoot": "src", 42 | "prefix": "app", 43 | "architect": { 44 | "build": { 45 | "builder": "ngx-build-plus:browser", 46 | "options": { 47 | "outputPath": "dist", 48 | "index": "src/index.html", 49 | "main": "src/main.ts", 50 | "polyfills": "src/polyfills.ts", 51 | "tsConfig": "tsconfig.app.json", 52 | "inlineStyleLanguage": "scss", 53 | "sourceMap": true, 54 | "styles": [ 55 | "node_modules/@taiga-ui/core/styles/taiga-ui-theme.less", 56 | "node_modules/@taiga-ui/core/styles/taiga-ui-fonts.less", 57 | "node_modules/@taiga-ui/styles/taiga-ui-global.less", 58 | "src/styles.scss" 59 | ], 60 | "assets": [ 61 | "src/_locales", 62 | "src/favicon.ico", 63 | "src/assets", 64 | "src/manifest.json", 65 | { 66 | "glob": "**/*", 67 | "input": "node_modules/@taiga-ui/icons/src", 68 | "output": "assets/taiga-ui/icons" 69 | }, 70 | { 71 | "glob": "**/*", 72 | "input": "node_modules/@taiga-ui/icons/src", 73 | "output": "assets/taiga-ui/icons" 74 | } 75 | ] 76 | }, 77 | "configurations": { 78 | "production": { 79 | "optimization": { 80 | "scripts": true, 81 | "styles": { 82 | "minify": true, 83 | "inlineCritical": false 84 | }, 85 | "fonts": true 86 | }, 87 | "budgets": [ 88 | { 89 | "type": "initial", 90 | "maximumWarning": "500kb", 91 | "maximumError": "1mb" 92 | }, 93 | { 94 | "type": "anyComponentStyle", 95 | "maximumWarning": "2kb", 96 | "maximumError": "4kb" 97 | } 98 | ], 99 | "fileReplacements": [ 100 | { 101 | "replace": "src/environments/environment.ts", 102 | "with": "src/environments/environment.prod.ts" 103 | } 104 | ] 105 | }, 106 | "development": { 107 | "deleteOutputPath": false, 108 | "buildOptimizer": false, 109 | "optimization": false, 110 | "vendorChunk": true, 111 | "extractLicenses": false, 112 | "namedChunks": true 113 | } 114 | }, 115 | "defaultConfiguration": "production" 116 | }, 117 | "serve": { 118 | "builder": "ngx-build-plus:dev-server", 119 | "configurations": { 120 | "production": { 121 | "browserTarget": "common:build:production" 122 | }, 123 | "development": { 124 | "browserTarget": "common:build:development" 125 | } 126 | }, 127 | "defaultConfiguration": "development" 128 | }, 129 | "extract-i18n": { 130 | "builder": "@angular-devkit/build-angular:extract-i18n", 131 | "options": { 132 | "browserTarget": "common:build" 133 | } 134 | }, 135 | "lint": { 136 | "builder": "@angular-eslint/builder:lint", 137 | "options": { 138 | "lintFilePatterns": [ 139 | "src/**/*.ts", 140 | "src/**/*.html" 141 | ] 142 | } 143 | } 144 | } 145 | } 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "obsidian-web-clipper", 3 | "packageManager": "yarn@3.3.0", 4 | "type": "module", 5 | "version": "1.0.4", 6 | "scripts": { 7 | "ng": "ng", 8 | "lint": "cd ./dist && web-ext lint", 9 | "lint:dev": "npm-run-all build:dev lint", 10 | "build": "npm-run-all build:angular build:content lint", 11 | "build:angular": "ng build --extra-webpack-config webpack.externals.cjs", 12 | "build:content": "esbuild src/content-scripts/index.ts --outfile=dist/content-script.js --bundle --format=esm --minify --sourcemap --target=firefox57", 13 | "build:dev": "ng build --stats-json --configuration development --extra-webpack-config webpack.externals.cjs", 14 | "build:dev:content": "esbuild src/content-scripts/index.ts --outfile=dist/content-script.js --bundle --format=esm --sourcemap --target=firefox57", 15 | "watch:angular": "ng build --stats-json --extra-webpack-config webpack.externals.cjs --watch --configuration development", 16 | "watch:content": "esbuild src/content-scripts/index.ts --outfile=dist/content-script.js --watch --bundle --format=esm --sourcemap --target=firefox57", 17 | "watch:package": "cd ./dist && web-ext run --verbose", 18 | "analyze": "webpack-bundle-analyzer dist/stats.json", 19 | "postinstall": "ngcc", 20 | "postinstall:bak": "ngcc" 21 | }, 22 | "private": true, 23 | "dependencies": { 24 | "@angular/animations": "^15.0.1", 25 | "@angular/cdk": "^15.0.0", 26 | "@angular/common": "^15.0.1", 27 | "@angular/compiler": "^15.0.1", 28 | "@angular/core": "^15.0.1", 29 | "@angular/elements": "15.0.1", 30 | "@angular/forms": "^15.0.1", 31 | "@angular/platform-browser": "^15.0.1", 32 | "@angular/platform-browser-dynamic": "^15.0.1", 33 | "@angular/router": "^15.0.1", 34 | "@availity/resolve-url": "^2.0.3", 35 | "@settingdust/article-extractor": "^0.3.0", 36 | "@taiga-ui/addon-doc": "^3.10.0", 37 | "@taiga-ui/addon-table": "^3.10.0", 38 | "@taiga-ui/addon-tablebars": "^3.10.0", 39 | "@taiga-ui/cdk": "^3.10.0", 40 | "@taiga-ui/core": "^3.10.0", 41 | "@taiga-ui/icons": "^3.10.0", 42 | "@taiga-ui/kit": "^3.10.0", 43 | "@taiga-ui/styles": "^3.10.0", 44 | "@tinkoff/ng-dompurify": "^4.0.0", 45 | "@webcomponents/custom-elements": "^1.5.1", 46 | "dompurify": "^2.4.1", 47 | "filenamify": "^5.1.1", 48 | "mustache": "^4.2.0", 49 | "node-html-markdown": "^1.2.2", 50 | "rxjs": "~7.5.7", 51 | "tinykeys": "^1.4.0", 52 | "tslib": "^2.4.1", 53 | "urlpattern-polyfill": "^6.0.2", 54 | "zone.js": "~0.12.0" 55 | }, 56 | "devDependencies": { 57 | "@angular-devkit/build-angular": "^15.0.0", 58 | "@angular-eslint/builder": "15.1.0", 59 | "@angular-eslint/eslint-plugin": "15.1.0", 60 | "@angular-eslint/eslint-plugin-template": "15.1.0", 61 | "@angular-eslint/schematics": "15.1.0", 62 | "@angular-eslint/template-parser": "15.1.0", 63 | "@angular/cli": "^15.0.0", 64 | "@angular/compiler-cli": "^15.0.1", 65 | "@types/dompurify": "^2.4.0", 66 | "@types/firefox-webext-browser": "^94.0.1", 67 | "@types/mustache": "^4", 68 | "@types/node": "~16", 69 | "@types/sanitize-html": "^2.6.2", 70 | "@typescript-eslint/eslint-plugin": "5.44.0", 71 | "@typescript-eslint/parser": "5.44.0", 72 | "esbuild": "^0.15.15", 73 | "eslint": "^8.28.0", 74 | "eslint-config-prettier": "^8.5.0", 75 | "eslint-plugin-prettier": "^4.2.1", 76 | "eslint-plugin-rxjs": "^5.0.2", 77 | "eslint-plugin-unicorn": "^45.0.0", 78 | "ngx-build-plus": "^15.0.0", 79 | "npm-run-all": "^4.1.5", 80 | "prettier": "^2.7.1", 81 | "typescript": "~4.8.4", 82 | "web-ext": "^7.3.1", 83 | "webpack": "^5.75.0", 84 | "webpack-bundle-analyzer": "^4.7.0" 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/_locales/en/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { 3 | "message": "Obsidian Web Clipper" 4 | }, 5 | "extensionDescription": { 6 | "message": "Export the page content to Obsidian as Markdown" 7 | }, 8 | "optionTabGeneral": { 9 | "message": "General" 10 | }, 11 | "optionTabShortcuts": { 12 | "message": "Shortcuts" 13 | }, 14 | "optionTabRules": { 15 | "message": "Rules" 16 | }, 17 | "optionGeneralApi": { 18 | "message": "Local REST API options" 19 | }, 20 | "optionApiUrl": { 21 | "message": "Endpoint" 22 | }, 23 | "optionApiUrlDescription": { 24 | "message": "Local REST API Endpoint. Host is your ip or domain, be `localhost`commonly. Port after the colon can be found in Obsidian > Local REST API options" 25 | }, 26 | "optionApiUrlPermission": { 27 | "message": "Request permission" 28 | }, 29 | "optionApiUrlPermissionDescription": { 30 | "message": "Have to request permission after change the endpoint for access" 31 | }, 32 | "optionApiToken": { 33 | "message": "Token" 34 | }, 35 | "optionApiTokenDescription": { 36 | "message": "Local REST API token,can be found in Obsidian > Local REST API > Token options" 37 | }, 38 | "optionVault": { 39 | "message": "Vault" 40 | }, 41 | "optionVaultDescription": { 42 | "message": "Optional, current/latest vault if empty" 43 | }, 44 | "optionPaths": { 45 | "message": "Paths" 46 | }, 47 | "optionShortcut": { 48 | "message": "Shortcut" 49 | }, 50 | "optionAction": { 51 | "message": "Action" 52 | }, 53 | "optionAddShortcut": { 54 | "message": "Add shortcut" 55 | }, 56 | "optionShortcutMemo": { 57 | "message": "Memo" 58 | }, 59 | "optionShortcutReadLater": { 60 | "message": "ReadLater" 61 | }, 62 | "optionProtocol": { 63 | "message": "Bind url protocol" 64 | }, 65 | "optionProtocolAlert": { 66 | "message": "Required for addon to work. If you don't know what is it. Click now." 67 | }, 68 | "action-option": { 69 | "message": "Open Options" 70 | }, 71 | "action-export": { 72 | "message": "Export to" 73 | }, 74 | "optionRuleAdd": { 75 | "message": "Add rule" 76 | }, 77 | "optionRulePatterns": { 78 | "message": "Patterns" 79 | }, 80 | "optionRuleAddPattern": { 81 | "message": "Add pattern" 82 | }, 83 | "optionRuleSelector": { 84 | "message": "Selector" 85 | }, 86 | "optionRuleSelectorDescription": { 87 | "message": "Css selector include the entire article" 88 | }, 89 | "optionRuleAddSelector": { 90 | "message": "Add selector" 91 | }, 92 | "optionRuleIgnored": { 93 | "message": "Ignored" 94 | }, 95 | "optionRuleIgnoredDescription": { 96 | "message": "Css selector for ignoring" 97 | }, 98 | "optionRuleAddIgnored": { 99 | "message": "Add ignored" 100 | }, 101 | "optionRuleNotification": { 102 | "message": "Higher priority for higher(newer) rules" 103 | }, 104 | "optionRuleTemplate": { 105 | "message": "Template" 106 | }, 107 | "errorNoServer": { 108 | "message": "Can't connect to server. Please confirm Obsidian is running, installed Local REST API plugin in Obsidian and check url option" 109 | }, 110 | "errorNoPermission": { 111 | "message": "Can't get extension permission. Please click request permission in options page" 112 | }, 113 | "errorNoToken": { 114 | "message": "No or wrong Local REST API token. Please find it in Obsidian settings" 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/_locales/zh_CN/messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "extensionName": { 3 | "message": "Obsidian 网页剪藏" 4 | }, 5 | "extensionDescription": { 6 | "message": "剪藏页面内容为 Markdown 到 Obsidian" 7 | }, 8 | "optionTabGeneral": { 9 | "message": "通用" 10 | }, 11 | "optionTabShortcuts": { 12 | "message": "快捷键" 13 | }, 14 | "optionTabRules": { 15 | "message": "规则" 16 | }, 17 | "optionGeneralApi": { 18 | "message": "Local REST API 设置" 19 | }, 20 | "optionApiUrl": { 21 | "message": "端点" 22 | }, 23 | "optionApiUrlDescription": { 24 | "message": "Local REST API 端点。域名应该是你的 IP,通常是 `localhost`。冒号后的端口可以在 Obsidian > Local REST API 的设置中找到" 25 | }, 26 | "optionApiUrlPermission": { 27 | "message": "请求权限" 28 | }, 29 | "optionApiUrlPermissionDescription": { 30 | "message": "修改端点后必须请求一次权限" 31 | }, 32 | "optionApiToken": { 33 | "message": "令牌" 34 | }, 35 | "optionApiTokenDescription": { 36 | "message": "Local REST API 令牌,可以在 Obsidian > Local REST API > Token 的设置中找到" 37 | }, 38 | "optionShortcut": { 39 | "message": "快捷键" 40 | }, 41 | "optionAction": { 42 | "message": "动作" 43 | }, 44 | "optionAddShortcut": { 45 | "message": "添加快捷键", 46 | "description": "Add a path" 47 | }, 48 | "optionShortcutMemo": { 49 | "message": "备忘录" 50 | }, 51 | "optionShortcutReadLater": { 52 | "message": "稍后读" 53 | }, 54 | "optionProtocol": { 55 | "message": "绑定 URL 协议" 56 | }, 57 | "optionProtocolAlert": { 58 | "message": "如果你不知道是什么,就点一下它" 59 | }, 60 | "action-option": { 61 | "message": "打开设置" 62 | }, 63 | "action-export": { 64 | "message": "导出到" 65 | }, 66 | "optionRuleAdd": { 67 | "message": "添加规则" 68 | }, 69 | "optionRulePatterns": { 70 | "message": "匹配模式" 71 | }, 72 | "optionRuleAddPattern": { 73 | "message": "添加匹配" 74 | }, 75 | "optionRuleSelector": { 76 | "message": "选择器" 77 | }, 78 | "optionRuleSelectorDescription": { 79 | "message": "包含全部文章内容的 CSS 选择器" 80 | }, 81 | "optionRuleAddSelector": { 82 | "message": "添加选择器" 83 | }, 84 | "optionRuleIgnored": { 85 | "message": "忽略" 86 | }, 87 | "optionRuleIgnoredDescription": { 88 | "message": "忽略的 CSS 选择器" 89 | }, 90 | "optionRuleAddIgnored": { 91 | "message": "添加忽略" 92 | }, 93 | "optionRuleNotification": { 94 | "message": "靠上(更新)的规则优先级更高" 95 | }, 96 | "optionRuleTemplate": { 97 | "message": "模板" 98 | }, 99 | "errorNoServer": { 100 | "message": "无法连接服务器,请确认 Obsidian 已启动,且装有 Local REST API 插件,并检查 URL 设置" 101 | }, 102 | "errorNoPermission": { 103 | "message": "无法获取扩展域名访问权限,请在设置中点击请求权限按钮" 104 | }, 105 | "errorNoToken": { 106 | "message": "Local REST API 令牌缺失或错误,请在 Obsidian 设置中查看插件设置" 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/action.ts: -------------------------------------------------------------------------------- 1 | export interface ActionData { 2 | receive?: T 3 | send?: U 4 | } 5 | 6 | export type ActionDataType = keyof ActionData 7 | 8 | export interface Actions { 9 | [key: string]: ActionData 10 | } 11 | 12 | export interface ActionMessage { 13 | action: U 14 | data?: T[U][V] 15 | } 16 | -------------------------------------------------------------------------------- /src/app/app-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { RouterModule, Routes } from '@angular/router' 3 | 4 | const routes: Routes = [ 5 | { path: 'background', loadChildren: () => import('./background/background.module').then((m) => m.BackgroundModule) }, 6 | { path: '', loadChildren: () => import('./options/options.module').then((m) => m.OptionsModule) } 7 | ] 8 | 9 | @NgModule({ 10 | imports: [RouterModule.forRoot(routes, { useHash: true })], 11 | exports: [RouterModule] 12 | }) 13 | export class AppRoutingModule {} 14 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | tui-root { 2 | min-height: 100vh; 3 | 4 | &[tuiMode="onDark"] { 5 | background-color: #212121; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core' 2 | import { Title } from '@angular/platform-browser' 3 | import { i18n } from './i18n.pipe' 4 | 5 | @Component({ 6 | selector: 'app-root', 7 | templateUrl: 'app.component.html', 8 | styleUrls: ['app.component.scss'] 9 | }) 10 | export class AppComponent { 11 | constructor(title: Title) { 12 | title.setTitle(i18n('extensionName')) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgDompurifySanitizer } from '@tinkoff/ng-dompurify' 2 | import { 3 | TUI_SANITIZER, 4 | TuiAlertModule, 5 | TuiDialogModule, 6 | TuiModeModule, 7 | TuiRootModule, 8 | TuiThemeNightModule 9 | } from '@taiga-ui/core' 10 | import { NgModule } from '@angular/core' 11 | import { BrowserModule } from '@angular/platform-browser' 12 | 13 | import { AppRoutingModule } from './app-routing.module' 14 | import { AppComponent } from './app.component' 15 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations' 16 | import { I18nPipe } from './i18n.pipe' 17 | import { HttpClientModule } from '@angular/common/http' 18 | 19 | if (!('URLPattern' in globalThis)) { 20 | await import('urlpattern-polyfill') 21 | } 22 | 23 | @NgModule({ 24 | declarations: [AppComponent], 25 | imports: [ 26 | BrowserModule, 27 | HttpClientModule, 28 | AppRoutingModule, 29 | BrowserAnimationsModule, 30 | TuiRootModule, 31 | TuiDialogModule, 32 | TuiAlertModule, 33 | TuiThemeNightModule, 34 | TuiModeModule 35 | ], 36 | providers: [{ provide: TUI_SANITIZER, useClass: NgDompurifySanitizer }, I18nPipe], 37 | bootstrap: [AppComponent] 38 | }) 39 | export class AppModule {} 40 | -------------------------------------------------------------------------------- /src/app/article-extractor.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core' 2 | import { combineLatestWith, defaultIfEmpty, from, of, tap } from 'rxjs' 3 | import { filter, map } from 'rxjs/operators' 4 | import { Rule } from './rule.service' 5 | import { contentSelectors, extract } from '@settingdust/article-extractor' 6 | import { relativeToAbsolute } from '@availity/resolve-url' 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class ArticleExtractorService { 12 | rules = (rules: Rule[]) => { 13 | contentSelectors.clear() 14 | for (const rule of rules) { 15 | for (const pattern of rule.patterns) 16 | if (rule.selector) contentSelectors.set(pattern, rule) 17 | } 18 | console.debug('[article-extractor:content-selectors]', contentSelectors) 19 | } 20 | 21 | extract = ({ url, document, selection }: ExportData) => 22 | from(extract(document, { url })).pipe( 23 | combineLatestWith( 24 | of(selection).pipe( 25 | filter((selection): selection is string => !!selection), 26 | map((it) => this.absolutifyDocument(new DOMParser().parseFromString(it, 'text/html'), url)), 27 | // eslint-disable-next-line unicorn/no-useless-undefined 28 | defaultIfEmpty(undefined) 29 | ) 30 | ), 31 | tap(([result, selection]) => (result.content = selection?.documentElement?.outerHTML ?? result?.content)), 32 | map(([result]) => { 33 | console.debug('[article-extractor]:', result) 34 | return result 35 | }) 36 | ) 37 | 38 | private absolutifyDocument = (document: Document, baseUrl = document.baseURI) => { 39 | // FIXME https://github.com/sindresorhus/eslint-plugin-unicorn/issues/1990 40 | // eslint-disable-next-line unicorn/prefer-spread 41 | for (const element of Array.from(document.querySelectorAll('a'))) { 42 | const href = element.getAttribute('href') 43 | if (href) element.setAttribute('href', relativeToAbsolute(href, baseUrl)) 44 | } 45 | 46 | // eslint-disable-next-line unicorn/prefer-spread 47 | for (const element of Array.from(document.querySelectorAll('img'))) { 48 | const source = element.dataset.src ?? element.getAttribute('src') 49 | if (source) element.setAttribute('src', relativeToAbsolute(source, baseUrl)) 50 | } 51 | 52 | return document 53 | } 54 | } 55 | 56 | export interface ExportData { 57 | document: string 58 | url: string 59 | selection?: string 60 | path?: string 61 | } 62 | -------------------------------------------------------------------------------- /src/app/background/background-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { RouterModule, Routes } from '@angular/router' 3 | import { BackgroundComponent } from './background.component' 4 | 5 | const routes: Routes = [{ path: '', component: BackgroundComponent }] 6 | 7 | @NgModule({ 8 | imports: [RouterModule.forChild(routes)], 9 | exports: [RouterModule] 10 | }) 11 | export class BackgroundRoutingModule {} 12 | -------------------------------------------------------------------------------- /src/app/background/background.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core' 2 | import { ExtensionService } from '../extension.service' 3 | import { ObsidianService } from './obsidian.service' 4 | import { catchError, of, throwError } from 'rxjs' 5 | import { map, switchMap, tap } from 'rxjs/operators' 6 | import { MarkdownService } from './markdown.service' 7 | import filenamify from 'filenamify' 8 | import { ArticleExtractorService } from '../article-extractor.service' 9 | import { ExportTemplateService } from '../export-template.service' 10 | import { i18n } from '../i18n.pipe' 11 | 12 | @Component({ 13 | selector: 'app-background', 14 | template: ``, 15 | styles: [] 16 | }) 17 | export class BackgroundComponent { 18 | constructor( 19 | extensionService: ExtensionService, 20 | obsidianService: ObsidianService, 21 | markdownService: MarkdownService, 22 | articleParserService: ArticleExtractorService, 23 | templateService: ExportTemplateService 24 | ) { 25 | extensionService.message 26 | .onAction('export') 27 | .pipe( 28 | tap((it) => console.debug('[action:export]:', it.message)), 29 | switchMap(({ message: { document, url: inputUrl, selection, path = '' }, sender }) => 30 | obsidianService.status().pipe( 31 | catchError(() => throwError(() => new Error(i18n('errorNoServer')))), 32 | switchMap((it) => { 33 | return it.authenticated ? of(true) : throwError(() => new Error(i18n('errorNoToken'))) 34 | }), 35 | switchMap(() => 36 | articleParserService.extract({ document, url: inputUrl, selection }).pipe( 37 | map((data) => (data.content ? { ...data, content: markdownService.convert(data.content) } : data)), 38 | switchMap((data) => 39 | templateService.get(data.url).pipe( 40 | switchMap((template) => { 41 | const finalPath = `${encodeURIComponent(path)}/${encodeURIComponent(filenamify(data.title))}.md` 42 | console.debug('[obsidian:create]', finalPath) 43 | return obsidianService.create( 44 | finalPath, 45 | data.content ? templateService.render(template, data) : undefined 46 | ) 47 | }) 48 | ) 49 | ) 50 | ) 51 | ), 52 | catchError((error: unknown) => { 53 | if (sender?.tab?.id) extensionService.message.action(sender.tab.id, 'error', error) 54 | console.warn(error) 55 | return of(null) 56 | }) 57 | ) 58 | ) 59 | ) 60 | .subscribe() 61 | 62 | extensionService.message.onAction('option').subscribe((data) => { 63 | console.debug('[action:option]') 64 | data.respond() 65 | return browser.tabs.create({ 66 | url: browser.runtime.getURL('index.html?#/(options:rules)'), 67 | active: true 68 | }) 69 | }) 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/app/background/background.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { CommonModule } from '@angular/common' 3 | 4 | import { BackgroundRoutingModule } from './background-routing.module' 5 | import { BackgroundComponent } from './background.component' 6 | 7 | @NgModule({ 8 | declarations: [BackgroundComponent], 9 | imports: [CommonModule, BackgroundRoutingModule], 10 | providers: [] 11 | }) 12 | export class BackgroundModule {} 13 | -------------------------------------------------------------------------------- /src/app/background/markdown.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core' 2 | import { NodeHtmlMarkdown } from 'node-html-markdown' 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class MarkdownService { 8 | convert = (html: string) => { 9 | const content = NodeHtmlMarkdown.translate(html, { 10 | preferNativeParser: true, 11 | textReplace: [[/ it.api))).subscribe((it) => { 21 | this.api = it 22 | console.debug(it) 23 | }) 24 | } 25 | 26 | /** 27 | * Fetch the REST server status 28 | */ 29 | status = () => { 30 | console.debug('[connecting]', this.api) 31 | return this.request<{ authenticated: boolean }>().pipe( 32 | catchError((error: unknown) => this.open().pipe(() => throwError(() => error))) 33 | ) 34 | } 35 | 36 | create = (filename: string, content?: string) => { 37 | console.debug('[obsidian:create:encoded]', filename) 38 | return this.request(`vault/${filename}`, 'put', { 39 | headers: { 40 | 'Content-Type': 'text/markdown' 41 | }, 42 | body: content 43 | }) 44 | } 45 | 46 | /** 47 | * Open the Obsidian using url scheme 48 | */ 49 | open = () => 50 | this.browserService.tab.create({ url: `${this.BASE_URL_SCHEMA}/` }).pipe( 51 | map(({ id }) => id), 52 | filter((id): id is number => !!id), 53 | switchMap((id) => this.browserService.tab.warmup(id)), 54 | switchMap((id) => browser.tabs.remove(id)) 55 | ) 56 | 57 | private header = () => ({ 58 | Authorization: `Bearer ${this.api?.token}` 59 | }) 60 | 61 | private request = , Body = undefined>( 62 | path = '', 63 | method: 'get' | 'post' | 'put' | 'delete' | 'patch' = 'get', 64 | options: { 65 | body?: Body 66 | headers?: { 67 | [header: string]: string | string[] 68 | } 69 | } = {} 70 | ) => >this.httpClient.request(method, `${this.api?.url}${path}`, { 71 | ...options, 72 | headers: { 73 | ...this.header(), 74 | ...options.headers 75 | } 76 | }) 77 | } 78 | -------------------------------------------------------------------------------- /src/app/export-template.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core' 2 | import { RuleService } from './rule.service' 3 | import defaultTemplate from '../assets/default.template' 4 | import render from 'mustache' 5 | import { ArticleExtractorService } from './article-extractor.service' 6 | // eslint-disable-next-line rxjs/no-internal 7 | import type { ObservedValueOf } from 'rxjs/internal/types' 8 | import { map, tap } from 'rxjs/operators' 9 | import { defaultIfEmpty } from 'rxjs' 10 | 11 | @Injectable({ 12 | providedIn: 'root' 13 | }) 14 | export class ExportTemplateService { 15 | constructor(private ruleService: RuleService) {} 16 | 17 | get defaultTemplate() { 18 | return defaultTemplate 19 | } 20 | 21 | get = (url: string) => 22 | this.ruleService.first(url).pipe( 23 | map((it) => it.template ?? this.defaultTemplate), 24 | defaultIfEmpty(this.defaultTemplate), 25 | tap((it) => console.debug('[template]', it)) 26 | ) 27 | 28 | render = (template: string, data: ObservedValueOf>) => 29 | render.render(template, data) 30 | } 31 | -------------------------------------------------------------------------------- /src/app/extension.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core' 2 | import { filter, map } from 'rxjs/operators' 3 | import { from, Observable } from 'rxjs' 4 | import { ActionData, ActionDataType, ActionMessage, Actions } from '../action' 5 | import { ExportData } from './article-extractor.service' 6 | import MessageSender = browser.runtime.MessageSender 7 | 8 | @Injectable({ 9 | providedIn: 'root' 10 | }) 11 | export class ExtensionService { 12 | // eslint-disable-next-line unicorn/consistent-function-scoping 13 | private $listener = new Observable((subscriber) => 14 | browser.runtime.onMessage.addListener((message, sender, respond) => subscriber.next({ message, sender, respond })) 15 | ) 16 | 17 | message = { 18 | onReceive: () => this.$listener as Observable>, 19 | onAction: (action: T) => 20 | this.message.onReceive().pipe( 21 | filter(({ message }) => message.action === action), 22 | map( 23 | ({ message, sender, respond }) => 24 | ({ 25 | message: message.data, 26 | sender, 27 | respond 28 | } as ContentMessageListener & { message: ContentActionMessage['data'] }) 29 | ) 30 | ), 31 | action: < 32 | T extends ContentAction, 33 | U extends ContentActionMessage['data'] = ContentActionMessage['data'] 34 | >( 35 | tab: number, 36 | action: T, 37 | data?: U 38 | ): Observable> => from(browser.tabs.sendMessage(tab, { action, data })) 39 | } 40 | 41 | tab = { 42 | create: (options: { url: string; active?: boolean }) => from(browser.tabs.create({ active: false, ...options })), 43 | warmup: (id: number) => from(browser.tabs.warmup(id)).pipe(map(() => id)) 44 | } 45 | 46 | private $change = new Observable<[changes: { [key: string]: browser.storage.StorageChange }, area: string]>( 47 | // eslint-disable-next-line unicorn/consistent-function-scoping 48 | (subscriber) => browser.storage.onChanged.addListener((change, area) => subscriber.next([change, area])) 49 | ) 50 | 51 | storage = { 52 | onChange: , U extends keyof T = keyof T>(name: 'local' | 'sync' | 'managed') => 53 | this.$change.pipe( 54 | filter(([, area]) => area === name), 55 | map(([change]) => >change) 56 | ) 57 | } 58 | } 59 | 60 | export interface ContentActions extends Actions { 61 | export: ActionData 62 | } 63 | 64 | export type ContentAction = keyof ContentActions 65 | 66 | export type ContentActionMessage = ActionMessage< 67 | ContentActions, 68 | T, 69 | U 70 | > 71 | 72 | type ContentMessageListener = { 73 | message: ContentActionMessage 74 | sender: MessageSender 75 | respond: (response?: ContentActionMessage) => void 76 | } 77 | -------------------------------------------------------------------------------- /src/app/i18n.pipe.ts: -------------------------------------------------------------------------------- 1 | import { Pipe, PipeTransform } from '@angular/core' 2 | 3 | export function i18n(value: string, ...arguments_: unknown[]) { 4 | const result = browser.i18n.getMessage(value, arguments_) 5 | return result?.length ? result : value 6 | } 7 | 8 | @Pipe({ 9 | name: 'i18n' 10 | }) 11 | export class I18nPipe implements PipeTransform { 12 | transform(value: string, ...arguments_: unknown[]) { 13 | return i18n(value, ...arguments_) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/app/option.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core' 2 | import { i18n } from './i18n.pipe' 3 | import { from, Observable, Subject, switchMap } from 'rxjs' 4 | import { ExtensionService } from './extension.service' 5 | import { map, tap } from 'rxjs/operators' 6 | import { Rule } from './rule.service' 7 | 8 | const defaultOptions = { 9 | shortcuts: [ 10 | { 11 | shortcut: 'Alt+o Alt+p', 12 | action: 'option' 13 | }, 14 | { 15 | shortcut: 'Alt+r Alt+l', 16 | action: 'export', 17 | path: i18n('optionShortcutReadLater') 18 | }, 19 | { 20 | shortcut: 'Alt+m Alt+o', 21 | action: 'export', 22 | path: i18n('optionShortcutMemo') 23 | } 24 | ], 25 | rules: [ 26 | { 27 | patterns: ['*://foo.bar/*', '*://exam.ple/*'], 28 | selector: ['#Article'], 29 | ignored: ['.foo', '.bar'] 30 | } 31 | ], 32 | api: { 33 | url: 'https://localhost:27124', 34 | token: '' 35 | } 36 | } 37 | 38 | export type Options = typeof defaultOptions 39 | type OptionKeys = keyof Options 40 | 41 | @Injectable({ 42 | providedIn: 'root' 43 | }) 44 | export class OptionService { 45 | get options() { 46 | return from(>browser.storage.local.get(defaultOptions)) 47 | } 48 | 49 | set options(value) { 50 | value 51 | .pipe( 52 | tap(({ api }) => { 53 | if (!api.url.endsWith('/')) api.url += '/' 54 | }) 55 | ) 56 | .subscribe((it) => browser.storage.local.set(it).then()) 57 | } 58 | 59 | set(value: { 60 | [key in T]: Options[key] 61 | }) { 62 | return from(browser.storage.local.set(value)) 63 | } 64 | 65 | get(key: T) { 66 | return from(>>browser.storage.local.get(key)).pipe(map((it) => it[key])) 67 | } 68 | 69 | private _onChange = new Subject>() 70 | get onChange() { 71 | return this._onChange 72 | } 73 | 74 | constructor(browserService: ExtensionService) { 75 | // Init the defaults 76 | from(browser.storage.local.get(defaultOptions)) 77 | .pipe( 78 | (it) => (this.options = >it), 79 | tap((it) => console.debug('[options:init]', it)) 80 | ) 81 | .subscribe() 82 | 83 | browserService.storage 84 | .onChange('local') 85 | .pipe( 86 | tap((changes) => { 87 | const result = {} 88 | for (const key in changes) { 89 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 90 | // @ts-ignore 91 | result[key] = changes[key]?.newValue 92 | } 93 | this._onChange.next(result) 94 | }), 95 | switchMap(() => this.options.pipe(tap((it) => console.debug('[options:change]', it)))) 96 | ) 97 | .subscribe() 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/app/options/general/general.component.html: -------------------------------------------------------------------------------- 1 | 2 | {{ 'optionGeneralApi' | i18n }} 3 | {{ 'optionApiUrlPermissionDescription' | i18n }} 4 | 5 | {{ 6 | 'optionApiUrl' | i18n 7 | }} 8 | 9 | {{ 'optionApiUrlPermission' | i18n }} 10 | 11 | 12 | {{ 13 | 'optionApiToken' | i18n 14 | }} 15 | 16 | -------------------------------------------------------------------------------- /src/app/options/general/general.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SettingDust/obsidian-web-clipper/b853a1dda78e8c548ee73d70678266fbf0c2bcc8/src/app/options/general/general.component.scss -------------------------------------------------------------------------------- /src/app/options/general/general.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core' 2 | import { FormBuilder, Validators } from '@angular/forms' 3 | import { EMPTY } from 'rxjs' 4 | import { switchMap, tap } from 'rxjs/operators' 5 | import { OptionService } from '../../option.service' 6 | import { i18n } from '../../i18n.pipe' 7 | 8 | @Component({ 9 | selector: 'app-general', 10 | templateUrl: './general.component.html', 11 | styleUrls: ['./general.component.scss'] 12 | }) 13 | export class GeneralComponent { 14 | form = this.fb.group({ 15 | url: this.fb.control('', Validators.required), 16 | token: this.fb.control('', Validators.required) 17 | }) 18 | 19 | constructor(private fb: FormBuilder, private optionService: OptionService) { 20 | this.optionService 21 | .get('api') 22 | .pipe( 23 | tap((it) => this.form.patchValue(it)), 24 | switchMap(() => 25 | this.form.valueChanges.pipe( 26 | switchMap((value) => 27 | value.url && value.token 28 | ? this.optionService.set({ 29 | api: { 30 | url: value.url, 31 | token: value.token 32 | } 33 | }) 34 | : EMPTY 35 | ) 36 | ) 37 | ) 38 | ) 39 | .subscribe() 40 | } 41 | 42 | request() { 43 | if (this.form.get('url')?.value) { 44 | // Remove all origins permission 45 | browser.permissions.getAll().then((it) => browser.permissions.remove({ origins: it.origins })) 46 | browser.permissions 47 | .request({ 48 | origins: [`${this.form.get('url')?.value}*`] 49 | }) 50 | .then((it) => { 51 | if (!it) throw new Error(i18n('errorNoPermission')) 52 | }) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/app/options/options-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { RouterModule, Routes } from '@angular/router' 3 | import { ShortcutsComponent } from './shortcuts/shortcuts.component' 4 | import { GeneralComponent } from './general/general.component' 5 | import { OptionsComponent } from './options.component' 6 | 7 | const routes: Routes = [ 8 | { path: '', component: OptionsComponent }, 9 | { path: 'general', component: GeneralComponent, outlet: 'options' }, 10 | { path: 'shortcuts', component: ShortcutsComponent, outlet: 'options' }, 11 | { path: 'rules', loadChildren: () => import('./rules/rules.module').then((m) => m.RulesModule), outlet: 'options' } 12 | ] 13 | 14 | @NgModule({ 15 | imports: [RouterModule.forChild(routes)], 16 | exports: [RouterModule] 17 | }) 18 | export class OptionsRoutingModule {} 19 | -------------------------------------------------------------------------------- /src/app/options/options.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ 'optionTabGeneral' | i18n }} 6 | 7 | 8 | 9 | {{ 'optionTabShortcuts' | i18n }} 10 | 11 | 12 | 13 | {{ 'optionTabRules' | i18n }} 14 | 15 | 16 | 17 | {{ 'optionProtocol' | i18n }} 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/app/options/options.component.scss: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SettingDust/obsidian-web-clipper/b853a1dda78e8c548ee73d70678266fbf0c2bcc8/src/app/options/options.component.scss -------------------------------------------------------------------------------- /src/app/options/options.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core' 2 | 3 | @Component({ 4 | selector: 'app-options', 5 | templateUrl: './options.component.html', 6 | styleUrls: ['./options.component.scss'] 7 | }) 8 | export class OptionsComponent {} 9 | -------------------------------------------------------------------------------- /src/app/options/options.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { CommonModule } from '@angular/common' 3 | 4 | import { OptionsRoutingModule } from './options-routing.module' 5 | import { OptionsComponent } from './options.component' 6 | import { ReactiveFormsModule } from '@angular/forms' 7 | import { 8 | TuiButtonModule, 9 | TuiDataListModule, 10 | TuiGroupModule, 11 | TuiHintModule, 12 | TuiLinkModule, 13 | TuiNotificationModule, 14 | TuiSvgModule, 15 | TuiTextfieldControllerModule 16 | } from '@taiga-ui/core' 17 | import { TuiDataListWrapperModule, TuiInputModule, TuiSelectModule, TuiTabsModule } from '@taiga-ui/kit' 18 | import { TuiTableModule } from '@taiga-ui/addon-table' 19 | import { ShortcutsComponent } from './shortcuts/shortcuts.component' 20 | import { GeneralComponent } from './general/general.component' 21 | import { SharedModule } from '../shared.module' 22 | 23 | @NgModule({ 24 | declarations: [OptionsComponent, ShortcutsComponent, GeneralComponent], 25 | imports: [ 26 | CommonModule, 27 | OptionsRoutingModule, 28 | ReactiveFormsModule, 29 | TuiInputModule, 30 | TuiLinkModule, 31 | TuiTableModule, 32 | TuiButtonModule, 33 | TuiTabsModule, 34 | TuiSvgModule, 35 | TuiHintModule, 36 | TuiTextfieldControllerModule, 37 | TuiDataListWrapperModule, 38 | TuiDataListModule, 39 | TuiSelectModule, 40 | SharedModule, 41 | TuiGroupModule, 42 | TuiNotificationModule 43 | ] 44 | }) 45 | export class OptionsModule {} 46 | -------------------------------------------------------------------------------- /src/app/options/rules/rules-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { RouterModule, Routes } from '@angular/router' 3 | import { RulesComponent } from './rules.component' 4 | 5 | const routes: Routes = [{ path: '', component: RulesComponent }] 6 | 7 | @NgModule({ 8 | imports: [RouterModule.forChild(routes)], 9 | exports: [RouterModule] 10 | }) 11 | export class RulesRoutingModule {} 12 | -------------------------------------------------------------------------------- /src/app/options/rules/rules.component.html: -------------------------------------------------------------------------------- 1 | 2 | {{ 'optionRuleNotification' | i18n }} 3 | 4 | {{ 'optionRuleAdd' | i18n }} 5 | 6 | 7 | 8 | 14 | 20 | 26 | 27 | {{ 'optionRulePatterns' | i18n }} 28 | 29 | 30 | 36 | 37 | 38 | {{ 'optionRuleAddPattern' | i18n }} 39 | 40 | 41 | 42 | {{ 'optionRuleSelector' | i18n }} 43 | 44 | 45 | 46 | 47 | 48 | 54 | 55 | 56 | {{ 'optionRuleAddSelector' | i18n }} 57 | 58 | 59 | 60 | {{ 'optionRuleIgnored' | i18n }} 61 | 62 | 63 | 64 | 65 | 66 | 72 | 73 | 74 | {{ 'optionRuleAddIgnored' | i18n }} 75 | 76 | 77 | 78 | {{ 'optionRuleTemplate' | i18n }} 79 | 80 | 81 | 82 | 83 | 84 | -------------------------------------------------------------------------------- /src/app/options/rules/rules.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | .inputs { 3 | display: grid; 4 | grid-template-columns: repeat(5, 1fr); 5 | grid-gap: 0.25rem; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/app/options/rules/rules.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core' 2 | import { ExtensionService } from '../../extension.service' 3 | import { AbstractControl, FormArray, FormBuilder, FormControl, FormGroup } from '@angular/forms' 4 | import { ActivatedRoute } from '@angular/router' 5 | import { filter, map, switchMap } from 'rxjs/operators' 6 | import type { Rule } from 'src/app/rule.service' 7 | import defaultTemplate from '../../../assets/default.template' 8 | import { ExportTemplateService } from '../../export-template.service' 9 | import { OptionService } from '../../option.service' 10 | 11 | type RuleForm = { 12 | patterns: FormArray> 13 | selector: FormArray> 14 | ignored: FormArray> 15 | template: FormControl 16 | } 17 | 18 | @Component({ 19 | selector: 'app-rules', 20 | templateUrl: './rules.component.html', 21 | styleUrls: ['./rules.component.scss'] 22 | }) 23 | export class RulesComponent implements OnInit { 24 | form = this.fb.group({ 25 | rules: this.fb.array>([]) 26 | }) 27 | 28 | constructor( 29 | private extensionService: ExtensionService, 30 | private fb: FormBuilder, 31 | private templateService: ExportTemplateService, 32 | private optionService: OptionService, 33 | route: ActivatedRoute 34 | ) { 35 | // TODO 符合 url 的规则排在上面 36 | } 37 | 38 | rulesForm = () => this.form.get('rules') as FormArray> 39 | 40 | patternsForm = (rule: AbstractControl) => rule.get('patterns') as FormArray> 41 | 42 | ignoredForm = (rule: AbstractControl) => rule.get('ignored') as FormArray> 43 | selectorForm = (rule: AbstractControl) => rule.get('selector') as FormArray> 44 | 45 | addRule(data?: Rule) { 46 | this.rulesForm().insert( 47 | 0, 48 | this.ruleToForm({ patterns: [''], template: this.templateService.defaultTemplate, ...data }) 49 | ) 50 | } 51 | 52 | addPatternControl(patterns: FormArray) { 53 | patterns.push(this.fb.control('')) 54 | } 55 | 56 | addIgnoredControl(ignored: FormArray) { 57 | ignored.push(this.fb.control('')) 58 | } 59 | 60 | addSelectorControl(selector: FormArray) { 61 | selector.push(this.fb.control('')) 62 | } 63 | 64 | ruleToForm = (data: Rule): FormGroup => 65 | this.fb.group({ 66 | patterns: this.fb.array((data.patterns ?? []).map((it) => this.fb.control(it, { nonNullable: true }))), 67 | selector: this.fb.array((data.selector ?? []).map((it) => this.fb.control(it, { nonNullable: true }))), 68 | ignored: this.fb.array((data.ignored ?? []).map((it) => this.fb.control(it, { nonNullable: true }))), 69 | template: this.fb.control(data.template) 70 | }) 71 | 72 | ngOnInit(): void { 73 | this.optionService 74 | .get('rules') 75 | .subscribe((rules) => 76 | this.form.setControl('rules', this.fb.array((rules ?? []).map((it: Rule) => this.ruleToForm(it)))) 77 | ) 78 | this.form.valueChanges 79 | .pipe( 80 | map((it) => 81 | it.rules 82 | ?.map( 83 | (rule) => 84 | { 85 | patterns: rule?.patterns?.filter((it) => it?.length) ?? [], 86 | selector: rule?.selector?.filter((it) => it?.length), 87 | ignored: rule?.ignored?.filter((it) => it?.length), 88 | template: rule?.template ?? defaultTemplate 89 | } 90 | ) 91 | .filter((rule) => rule.patterns?.length) 92 | ), 93 | filter((it): it is Rule[] => !!it), 94 | switchMap((value) => this.optionService.set({ rules: value })) 95 | ) 96 | .subscribe() 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/app/options/rules/rules.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { CommonModule } from '@angular/common' 3 | 4 | import { RulesRoutingModule } from './rules-routing.module' 5 | import { RulesComponent } from './rules.component' 6 | import { ReactiveFormsModule } from '@angular/forms' 7 | import { TuiTableModule } from '@taiga-ui/addon-table' 8 | import { TuiAccordionModule, TuiBadgeModule, TuiInputModule, TuiTextAreaModule } from '@taiga-ui/kit' 9 | import { 10 | TuiButtonModule, 11 | TuiGroupModule, 12 | TuiHintModule, 13 | TuiNotificationModule, 14 | TuiTextfieldControllerModule, 15 | TuiTooltipModule 16 | } from '@taiga-ui/core' 17 | import { SharedModule } from '../../shared.module' 18 | 19 | @NgModule({ 20 | declarations: [RulesComponent], 21 | imports: [ 22 | CommonModule, 23 | RulesRoutingModule, 24 | ReactiveFormsModule, 25 | TuiTableModule, 26 | TuiAccordionModule, 27 | TuiButtonModule, 28 | SharedModule, 29 | TuiBadgeModule, 30 | TuiGroupModule, 31 | TuiInputModule, 32 | TuiTextfieldControllerModule, 33 | TuiHintModule, 34 | TuiTooltipModule, 35 | TuiNotificationModule, 36 | TuiTextAreaModule 37 | ] 38 | }) 39 | export class RulesModule {} 40 | -------------------------------------------------------------------------------- /src/app/options/shortcuts/shortcuts.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ 'optionShortcut' | i18n }} 6 | {{ 'optionAction' | i18n }} 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | {{ 'optionAddShortcut' | i18n }} 33 | 34 | 35 | 36 | 37 | 38 | 39 | {{ 'action-' + action | i18n }} 40 | 41 | 42 | -------------------------------------------------------------------------------- /src/app/options/shortcuts/shortcuts.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | table { 3 | width: 100%; 4 | 5 | .tui-table__th { 6 | font: var(--tui-font-text-m); 7 | } 8 | 9 | .action-column { 10 | display: flex; 11 | 12 | & > * { 13 | width: 100%; 14 | } 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/options/shortcuts/shortcuts.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core' 2 | import { AbstractControl, UntypedFormArray, UntypedFormBuilder, Validators } from '@angular/forms' 3 | import { concat, from, of } from 'rxjs' 4 | import { map, switchMap, tap } from 'rxjs/operators' 5 | 6 | @Component({ 7 | selector: 'app-shortcuts', 8 | templateUrl: './shortcuts.component.html', 9 | styleUrls: ['./shortcuts.component.scss'] 10 | }) 11 | export class ShortcutsComponent { 12 | form = this.fb.group({ 13 | shortcuts: this.fb.array([]) 14 | }) 15 | actions: string[] = [] 16 | 17 | constructor(private fb: UntypedFormBuilder) { 18 | from(browser.storage.local.get('shortcuts')) 19 | .pipe( 20 | map((it) => it.shortcuts as [{ shortcut: string; action: string } & unknown]), 21 | tap((it) => this.form.setControl('shortcuts', this.fb.array(it.map((data) => this.fb.group(data))))), 22 | switchMap((it) => 23 | from(it).pipe( 24 | tap((data) => { 25 | if (!this.actions.includes(data.action)) this.actions.push(data.action) 26 | }) 27 | ) 28 | ), 29 | switchMap(() => 30 | this.form.valueChanges.pipe( 31 | map((it) => it.shortcuts.filter((object: { shortcut: unknown }) => object.shortcut)), 32 | switchMap((value) => browser.storage.local.set({ shortcuts: value })) 33 | ) 34 | ) 35 | ) 36 | .subscribe() 37 | } 38 | 39 | get shortcuts() { 40 | return this.form.get('shortcuts') as UntypedFormArray 41 | } 42 | 43 | addShortcut(data?: { shortcut: string; action: string } & unknown) { 44 | this.shortcuts.push(this.fb.group(this.shortcutToForm({ shortcut: '', action: 'option', ...data }))) 45 | } 46 | 47 | shortcutToForm = (data: { shortcut: string; action: string } & unknown) => 48 | this.fb.group({ 49 | shortcut: [data.shortcut, Validators.required], 50 | action: [data.action] 51 | }) 52 | 53 | $shortcutChange = (shortcut: AbstractControl) => 54 | concat(of(shortcut.get('action')?.value), from(shortcut.get('action')?.valueChanges ?? '')) 55 | } 56 | -------------------------------------------------------------------------------- /src/app/rule.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core' 2 | import { filter, from, switchMap, take } from 'rxjs' 3 | import { ArticleExtractorService } from './article-extractor.service' 4 | import { OptionService } from './option.service' 5 | import { tap } from 'rxjs/operators' 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class RuleService { 11 | constructor(private optionService: OptionService, articleParserService: ArticleExtractorService) { 12 | optionService.onChange.subscribe(({ rules }) => { 13 | if (rules) articleParserService.rules(rules) 14 | }) 15 | } 16 | 17 | /** 18 | * All rule that success with url 19 | * @param url 20 | */ 21 | get = (url: string) => 22 | this.optionService 23 | .get('rules') 24 | .pipe( 25 | switchMap((rules) => 26 | from(rules).pipe(filter((rule) => rule.patterns.some((it) => new URLPattern(it).test(url)))) 27 | ) 28 | ) 29 | 30 | first = (url: string) => 31 | this.get(url).pipe( 32 | take(1), 33 | tap((it) => console.debug('[rule]', it)) 34 | ) 35 | } 36 | 37 | export interface Rule { 38 | patterns: string[] 39 | selector?: string[] 40 | ignored?: string[] 41 | template?: string 42 | } 43 | -------------------------------------------------------------------------------- /src/app/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core' 2 | import { CommonModule } from '@angular/common' 3 | import { I18nPipe } from './i18n.pipe' 4 | 5 | @NgModule({ 6 | declarations: [I18nPipe], 7 | exports: [I18nPipe], 8 | imports: [CommonModule] 9 | }) 10 | export class SharedModule {} 11 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SettingDust/obsidian-web-clipper/b853a1dda78e8c548ee73d70678266fbf0c2bcc8/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/default.template: -------------------------------------------------------------------------------- 1 | # {{title}} 2 | *[{{&url}}]({{&url}})* 3 | {{#author}}Author: {{^author.url}}{{&author.name}}{{/author.url}}{{#author.url}}[{{&author.name}}]({{&author.url}}){{/author.url}}{{/author}} 4 | {{#date}}{{#date.published}}Published: {{date.published}}{{/date.published}} {{#date.modified}} 5 | Modified: {{date.modified}}{{/date.modified}}{{/date}} 6 | {{&content}} 7 | -------------------------------------------------------------------------------- /src/content-scripts/background-listener.ts: -------------------------------------------------------------------------------- 1 | import { filter, from, Observable } from 'rxjs' 2 | import { ActionData, ActionDataType, ActionMessage, Actions } from '../action' 3 | import { ExportBackgroundAction } from './export-action' 4 | import { ErrorBackgroundAction } from './error-action' 5 | import MessageSender = browser.runtime.MessageSender 6 | 7 | export interface BackgroundActions extends Actions { 8 | export: ExportBackgroundAction 9 | error: ErrorBackgroundAction 10 | option: ActionData 11 | } 12 | 13 | type BackgroundAction = keyof BackgroundActions 14 | 15 | type BackgroundActionMessage = ActionMessage< 16 | BackgroundActions, 17 | T, 18 | U 19 | > 20 | 21 | type BackgroundMessageListener = { 22 | message: BackgroundActionMessage 23 | sender: MessageSender 24 | respond: (response?: BackgroundActionMessage) => void 25 | } 26 | 27 | const $listener = new Observable((subscriber) => 28 | browser.runtime.onMessage.addListener((message, sender, respond) => subscriber.next({ message, sender, respond })) 29 | ) 30 | 31 | const $background = { 32 | message: { 33 | listener: () => $listener as Observable>, 34 | actionListener: (action: T) => 35 | $background.message.listener().pipe(filter(({ message }) => message.action === action)), 36 | action: ['data']>( 37 | action: T, 38 | data?: U 39 | ): Observable> => { 40 | console.debug(`[obsidian-web-clipper:action:${action}]`, data) 41 | return from(browser.runtime.sendMessage({ action, data })) 42 | } 43 | } 44 | } 45 | 46 | export default $background 47 | -------------------------------------------------------------------------------- /src/content-scripts/browser.ts: -------------------------------------------------------------------------------- 1 | import { Observable } from 'rxjs' 2 | import { filter, map } from 'rxjs/operators' 3 | 4 | const change$ = new Observable<[changes: { [key: string]: browser.storage.StorageChange }, area: string]>( 5 | (subscriber) => browser.storage.onChanged.addListener((change, area) => subscriber.next([change, area])) 6 | ) 7 | 8 | export const storage = { 9 | change: (name: 'local' | 'sync' | 'managed') => 10 | change$.pipe( 11 | filter(([, area]) => area === name), 12 | map(([change]) => change) 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /src/content-scripts/error-action.ts: -------------------------------------------------------------------------------- 1 | import { ActionData } from '../action' 2 | 3 | export type ErrorBackgroundAction = ActionData< 4 | { 5 | message: unknown 6 | }, 7 | undefined 8 | > 9 | -------------------------------------------------------------------------------- /src/content-scripts/export-action.ts: -------------------------------------------------------------------------------- 1 | import { ActionData } from '../action' 2 | 3 | export type ExportBackgroundAction = ActionData< 4 | undefined, 5 | { 6 | document: string 7 | url: string 8 | selection?: string 9 | path: string 10 | } 11 | > 12 | -------------------------------------------------------------------------------- /src/content-scripts/index.ts: -------------------------------------------------------------------------------- 1 | import './background-listener' 2 | import './shortcuts' 3 | import $background from './background-listener' 4 | 5 | console.debug('[obsidian-web-clipper]: Loaded') 6 | 7 | $background.message.actionListener('error').subscribe(({ message, respond }) => { 8 | console.warn('[obsidian-web-clipper:error]', message) 9 | const finalMessage = typeof message === 'string' ? message : message?.data?.message 10 | if (finalMessage) { 11 | alert(`[Obsidian Web Clipper] ${finalMessage}`) 12 | } 13 | respond() 14 | }) 15 | -------------------------------------------------------------------------------- /src/content-scripts/shortcuts.ts: -------------------------------------------------------------------------------- 1 | import keybindings, { KeyBindingMap, KeyBindingOptions } from 'tinykeys' 2 | import { concat, from, reduce, switchMap } from 'rxjs' 3 | import { storage } from './browser' 4 | import { filter, map, tap } from 'rxjs/operators' 5 | import $background from './background-listener' 6 | import { getSelectionHtml } from './utils' 7 | 8 | // eslint-disable-next-line @typescript-eslint/no-empty-function 9 | export let unregisterKeys: () => void = () => {} 10 | 11 | export const registerKeys = ( 12 | target: Window | HTMLElement, 13 | keyBindingMap: KeyBindingMap, 14 | options: KeyBindingOptions = {} 15 | ) => (unregisterKeys = keybindings(target, keyBindingMap, options)) 16 | 17 | const actions = { 18 | export: (data: unknown) => 19 | $background.message.action('export', { 20 | document: document.documentElement.outerHTML, 21 | url: window.location.href, 22 | selection: getSelectionHtml(), 23 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 24 | // @ts-ignore 25 | path: data?.path ?? '' 26 | }), 27 | option: () => $background.message.action('option') 28 | } 29 | 30 | concat<({ shortcut: string; action: 'export' | 'option'; [key: string]: unknown }[] | undefined)[]>( 31 | // eslint-disable-next-line unicorn/prefer-top-level-await 32 | browser.storage.local.get('shortcuts').then((it) => it.shortcuts), 33 | storage.change('local').pipe(map((change) => change?.shortcuts?.newValue)) 34 | ) 35 | .pipe( 36 | filter((it): it is NonNullable => !!it), 37 | tap(() => unregisterKeys()), 38 | tap((it) => console.debug('[obsidian-web-clipper:shortcut:register]', it)), 39 | switchMap((it) => 40 | from(it).pipe( 41 | reduce( 42 | (accumulator, value) => ({ 43 | ...accumulator, 44 | [value.shortcut]: () => { 45 | console.debug('[obsidian-web-clipper:shortcut]', value) 46 | return actions[value.action](value) 47 | } 48 | }), 49 | {} 50 | ) 51 | ) 52 | ) 53 | ) 54 | .subscribe((data) => registerKeys(window, data)) 55 | -------------------------------------------------------------------------------- /src/content-scripts/utils.ts: -------------------------------------------------------------------------------- 1 | export function getSelectionHtml() { 2 | const sel = window.getSelection() 3 | if (sel?.rangeCount) { 4 | const container = document.createElement('div') 5 | for (let index = 0; index < sel.rangeCount; ++index) { 6 | container.append(sel.getRangeAt(index).cloneContents()) 7 | } 8 | return container.innerHTML?.length ? container.innerHTML : undefined 9 | } 10 | return 11 | } 12 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | } 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | } 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/SettingDust/obsidian-web-clipper/b853a1dda78e8c548ee73d70678266fbf0c2bcc8/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | {{ "extensionName" | i18n }} 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core' 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic' 3 | 4 | import { AppModule } from './app/app.module' 5 | import { environment } from './environments/environment' 6 | 7 | if (environment.production) { 8 | enableProdMode() 9 | } 10 | 11 | platformBrowserDynamic() 12 | .bootstrapModule(AppModule) 13 | .catch((err) => console.error(err)) 14 | -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "__MSG_extensionName__", 4 | "version": "1.0.4", 5 | "description": "__MSG_extensionDescription__", 6 | "permissions": [ 7 | "tabs", 8 | "storage" 9 | ], 10 | "optional_permissions": [ 11 | "" 12 | ], 13 | "background": { 14 | "page": "index.html?#/background", 15 | "persistent": true 16 | }, 17 | "content_scripts": [ 18 | { 19 | "js": [ 20 | "content-script.js" 21 | ], 22 | "matches": [ 23 | "" 24 | ] 25 | } 26 | ], 27 | "options_ui": { 28 | "page": "index.html?#/(options:general)", 29 | "open_in_tab": true 30 | }, 31 | "browser_specific_settings": { 32 | "gecko": { 33 | "id": "{f9ba18c5-f3fe-4521-868f-981520a55892}" 34 | } 35 | }, 36 | "default_locale": "en" 37 | } 38 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/guide/browser-support 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** 22 | * By default, zone.js will patch all possible macroTask and DomEvents 23 | * user can disable parts of macroTask/DomEvents patch by setting following flags 24 | * because those flags need to be set before `zone.js` being loaded, and webpack 25 | * will put import in the top of bundle, so user need to create a separate file 26 | * in this directory (for example: zone-flags.ts), and put the following flags 27 | * into that file, and then add the following code before importing zone.js. 28 | * import './zone-flags'; 29 | * 30 | * The flags allowed in zone-flags.ts are listed here. 31 | * 32 | * The following flags will work for all browsers. 33 | * 34 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 35 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 36 | * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 37 | * 38 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 39 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 40 | * 41 | * (window as any).__Zone_enable_cross_context_check = true; 42 | * 43 | */ 44 | 45 | /*************************************************************************************************** 46 | * Zone JS is required by default for Angular itself. 47 | */ 48 | import 'zone.js' // Included with Angular CLI. 49 | 50 | /*************************************************************************************************** 51 | * APPLICATION IMPORTS 52 | */ 53 | ;(window as any)['global'] = window 54 | -------------------------------------------------------------------------------- /src/production/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "__MSG_extensionName__", 4 | "version": "0.2.0", 5 | "description": "__MSG_extensionDescription__", 6 | "permissions": ["tabs", "storage"], 7 | "background": { 8 | "page": "index.html?#/background", 9 | "persistent": true 10 | }, 11 | "content_scripts": [ 12 | { 13 | "js": ["content-script/content-script.js"], 14 | "matches": [""] 15 | } 16 | ], 17 | "options_ui": { 18 | "page": "index.html?#/options" 19 | }, 20 | "browser_specific_settings": { 21 | "gecko": { 22 | "id": "{f9ba18c5-f3fe-4521-868f-981520a55892}" 23 | } 24 | }, 25 | "default_locale": "en" 26 | } 27 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | .flex-placeholder { 3 | flex: 1; 4 | } 5 | -------------------------------------------------------------------------------- /src/template.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.template' { 2 | const content: string 3 | export default content 4 | } 5 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "extends": "./tsconfig.json", 4 | "compilerOptions": { 5 | "outDir": "./out-tsc/app", 6 | "module": "ES2022", 7 | "target": "ES2022", 8 | "useDefineForClassFields": false 9 | }, 10 | "files": ["src/main.ts", "src/polyfills.ts"], 11 | "include": ["src/**/*.d.ts"] 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */ 2 | { 3 | "compileOnSave": false, 4 | "compilerOptions": { 5 | "allowSyntheticDefaultImports": true, 6 | "baseUrl": "./", 7 | "outDir": "./dist/out-tsc", 8 | "forceConsistentCasingInFileNames": true, 9 | "strict": true, 10 | "noImplicitReturns": true, 11 | "noFallthroughCasesInSwitch": true, 12 | "sourceMap": true, 13 | "declaration": false, 14 | "downlevelIteration": true, 15 | "experimentalDecorators": true, 16 | "moduleResolution": "node", 17 | "importHelpers": true, 18 | "target": "ES2020", 19 | "module": "commonjs", 20 | "lib": [ 21 | "ES2018", 22 | "DOM" 23 | ], 24 | "skipLibCheck": true, 25 | "typeRoots": [ 26 | "node_modules/@types", 27 | "node_modules/@settingdust/article-extractor/src/@types" 28 | ] 29 | }, 30 | "angularCompilerOptions": { 31 | "enableI18nLegacyMessageIdFormat": false, 32 | "strictInjectionParameters": true, 33 | "strictInputAccessModifiers": true, 34 | "strictTemplates": true 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /webpack.externals.cjs: -------------------------------------------------------------------------------- 1 | const { Configuration, DefinePlugin } = require('webpack') 2 | 3 | /** 4 | * @type {Configuration} 5 | */ 6 | module.exports = { 7 | module: { 8 | rules: [{ test: /\.template$/, type: 'asset/source' }] 9 | }, 10 | externals: { 11 | 'node:path': {} 12 | }, 13 | experiments: { 14 | topLevelAwait: true 15 | }, 16 | plugins: [ 17 | new DefinePlugin({ 18 | 'process.env.LOG_PERF': false 19 | }) 20 | ] 21 | } 22 | --------------------------------------------------------------------------------