├── .ci ├── .eslintrc ├── AGPL ├── checkDeletedTxt.sh ├── eslint-plugin-zotero-translator │ ├── bin │ │ └── teslint.js │ ├── index.js │ ├── lib │ │ └── rules │ │ │ ├── header-last-updated.js │ │ │ ├── header-translator-id.js │ │ │ ├── header-translator-type.js │ │ │ ├── header-valid-json.js │ │ │ ├── license.js │ │ │ ├── no-for-each.js │ │ │ ├── not-executable.js │ │ │ ├── prefer-index-of.js │ │ │ ├── robust-query-selector.js │ │ │ ├── test-cases.js │ │ │ └── translator-framework.js │ ├── package-lock.json │ ├── package.json │ └── processor.js ├── helper.sh ├── lint.sh ├── pull-request-check │ ├── check-pull-request.sh │ ├── selenium-test.js │ ├── translator-server.js │ └── xvfb-run-chrome └── updateTypes.mjs ├── .editorconfig ├── .eslintrc ├── .github ├── ISSUE_TEMPLATE │ ├── T1_bug.yaml │ ├── T2_enhancement.yaml │ └── T3_new_translator.yaml └── workflows │ ├── ci.yml │ ├── main.yml │ └── syncCI.yml ├── .gitignore ├── .vscode ├── settings.json └── tasks.json ├── Angle.js ├── Baidu Baike.js ├── Baidu Scholar.js ├── Belt and Road Database.js ├── BibTeX.js ├── BiliBili.js ├── CCPINFO.js ├── CHINESE JOURNAL OF LAW.js ├── CNBKSY.js ├── CNKI CHKD.js ├── CNKI Gongjushu.js ├── CNKI Industry.js ├── CNKI Law.js ├── CNKI RefWorks.js ├── CNKI Refer.js ├── CNKI Scholar.js ├── CNKI TIKS.js ├── CNKI e-Books.js ├── CNKI thinker.js ├── CNKI.js ├── CNSDA.js ├── CQVIP Knowledge.js ├── CQVIP Qikan.js ├── CQVIP.js ├── CSDN.js ├── China Judgements Online.js ├── China Social Science Library.js ├── ChinaXiv.js ├── Cubox.js ├── Dangdang.js ├── Douban.js ├── Duxiu.js ├── E-Tiller.js ├── Encyclopedia of China 3rd.js ├── Founder.js ├── GFSOSO.js ├── Jd.js ├── Jikan Full Text Database.js ├── LICENSE ├── Lawbank.js ├── MagTech.js ├── Modern History.js ├── NDLTD.js ├── NTU Digital Library of Buddhist Studies.js ├── National Public Service Platform for Standards Information - China.js ├── National Science and Technology Library - China.js ├── National Science and Technology Report Service - China.js ├── National Standards Open System - China.js ├── Ncpssd.js ├── PKULaw.js ├── PatentStar.js ├── People's Daily Database.js ├── People's Daily Epaper.js ├── People's Daily Online.js ├── Pishu Database.js ├── ProQuestCN Thesis.js ├── Pss-System.js ├── PubScholar.js ├── Publications Data Center - China.js ├── QStheory.js ├── RDFYBK.js ├── README.md ├── RHHZ.js ├── RefWorks Tagged.js ├── Rural Studies Database.js ├── SKCTK.js ├── Samson.js ├── SciEngine.js ├── Science Reading.js ├── Sina Weibo.js ├── Soopat.js ├── Spc.org.cn.js ├── Standard Full-text Database - NLC.js ├── SuperLib.js ├── TOAJ.js ├── Wanfang Data.js ├── Wanfang Med.js ├── Weixin.js ├── Wenjin.js ├── Xinhuanet.js ├── Yiigle.js ├── Zhihu.js ├── Zhihuiya.js ├── Zhizhen.js ├── chaoxingqikan.js ├── data ├── dashboard.json ├── data.json ├── translators.json └── updateJSON.js ├── deleted.txt ├── doc.taixueshu.js ├── dpaper.js ├── epaper.gmw.cn.js ├── flk.npc.gov.cn.js ├── gov.cn Policy.js ├── incoPat.js ├── index.d.ts ├── jsconfig.json ├── package-lock.json ├── package.json ├── pm.tsgyun.js ├── sanmin.com.tw.js ├── sharing.com.tw.js ├── stats.gov.cn.js ├── xiaoyuzhoufm.js └── zhangqiaokeyan.js /.ci/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | }, 5 | "parserOptions": { 6 | "ecmaVersion": 2018 7 | }, 8 | "rules": { 9 | "notice/notice": "off" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /.ci/AGPL: -------------------------------------------------------------------------------- 1 | /* 2 | ***** BEGIN LICENSE BLOCK ***** 3 | 4 | Copyright © ${ period } ${ holder } 5 | 6 | This file is part of Zotero. 7 | 8 | Zotero is free software: you can redistribute it and/or modify 9 | it under the terms of the GNU Affero General Public License as published by 10 | the Free Software Foundation, either version 3 of the License, or 11 | (at your option) any later version. 12 | 13 | Zotero is distributed in the hope that it will be useful, 14 | but WITHOUT ANY WARRANTY; without even the implied warranty of 15 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 16 | GNU Affero General Public License for more details. 17 | 18 | You should have received a copy of the GNU Affero General Public License 19 | along with Zotero. If not, see . 20 | 21 | ***** END LICENSE BLOCK ***** 22 | */ 23 | -------------------------------------------------------------------------------- /.ci/checkDeletedTxt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | SCRIPT_DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 4 | . "$SCRIPT_DIR/helper.sh" 5 | cd "$SCRIPT_DIR" 6 | 7 | MASTER="master" 8 | BRANCH=$(git rev-parse --abbrev-ref HEAD) 9 | 10 | if [[ "$BRANCH" = "$MASTER" ]];then 11 | echo "${color_warn}skip${color_reset} - Can only check deleted.txt when not on '$MASTER' branch" 12 | exit 0 13 | fi 14 | 15 | if ! git branch -a | grep -q "$MASTER"; then 16 | echo "${color_warn}skip${color_reset} - Can only check deleted.txt when '$MASTER' branch is also available for comparison" 17 | exit 0 18 | fi 19 | 20 | main() { 21 | local IFS=$'\n' 22 | declare -a deletions=() 23 | local failed=0 24 | # Find all deleted js files 25 | deletions+=($(git diff-index --diff-filter=D --name-only --find-renames $MASTER|grep -v '\.ci'|grep 'js$')) 26 | if (( ${#deletions[@]} > 0 ));then 27 | for f in "${deletions[@]}";do 28 | local id=$(git show $MASTER:"$f" | get_translator_id) 29 | if ! grep -qF "$id" '../deleted.txt';then 30 | echo "${color_notok}not ok${color_reset} - $id ($f) should be added to deleted.txt" 31 | (( failed += 1)) 32 | fi 33 | done 34 | curVersion=$(head -n1 "../deleted.txt"|grep -o '[0-9]\+') 35 | origVersion=$(git show "$MASTER:deleted.txt"|head -n1|grep -o '[0-9]\+') 36 | if (( curVersion <= origVersion ));then 37 | echo "${color_notok}not ok${color_reset} - version in deleted.txt needs to be increased" 38 | (( failed += 1)) 39 | fi 40 | fi 41 | if [[ "$failed" = 0 ]];then 42 | echo "${color_ok}ok${color_reset} - deleted.txt" 43 | fi 44 | exit $failed 45 | } 46 | main 47 | -------------------------------------------------------------------------------- /.ci/eslint-plugin-zotero-translator/bin/teslint.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | const path = require('path'); 6 | 7 | process.argv = process.argv.map(arg => arg === '--output-json' ? [ '--format', 'json', '--output-file' ] : arg).flat(); 8 | 9 | require('../../../node_modules/.bin/eslint') 10 | -------------------------------------------------------------------------------- /.ci/eslint-plugin-zotero-translator/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @fileoverview Checks Zotero translators for errors and recommended style 3 | * @author Emiliano Heyns 4 | */ 5 | 6 | 'use strict'; 7 | 8 | const requireDir = require('require-dir'); 9 | 10 | module.exports = { 11 | rules: requireDir('./lib/rules'), 12 | processors: { 13 | translator: require('./processor'), 14 | }, 15 | }; 16 | -------------------------------------------------------------------------------- /.ci/eslint-plugin-zotero-translator/lib/rules/header-last-updated.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { parsed, header } = require('../../processor').support; 4 | 5 | module.exports = { 6 | meta: { 7 | type: 'problem', 8 | docs: { 9 | description: 'enforce valid lastUpdated in header', 10 | category: 'Possible Errors', 11 | }, 12 | fixable: 'code', 13 | }, 14 | 15 | create: function (context) { 16 | return { 17 | Program: function (node) { 18 | const filename = context.getFilename(); 19 | const translator = parsed(filename); 20 | if (!translator || !translator.header.fields) return; // regular js source, or header is invalid 21 | 22 | const lastUpdated = header(node).properties.find(p => p.key.value === 'lastUpdated'); 23 | 24 | if (!lastUpdated) { 25 | context.report({ 26 | loc: { start: { line: 1, column: 1 } }, 27 | message: 'Header needs lastUpdated field', 28 | }); 29 | return; 30 | } 31 | 32 | const format = date => date.toISOString().replace('T', ' ').replace(/\..*/, ''); 33 | const now = format(new Date()); 34 | const fix = fixer => fixer.replaceText(lastUpdated.value, `"${now}"`); 35 | 36 | if (typeof lastUpdated.value.value !== 'string' || !lastUpdated.value.value.match(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}$/)) { 37 | context.report({ 38 | node: lastUpdated.value, 39 | message: `lastUpdated field must be a string in YYYY-MM-DD HH:MM:SS format`, 40 | fix, 41 | }); 42 | return; 43 | } 44 | 45 | if (translator.lastUpdated && translator.lastUpdated > lastUpdated.value.value) { 46 | context.report({ 47 | node: lastUpdated.value, 48 | message: `lastUpdated field must be updated to be > ${translator.lastUpdated} to push to clients`, 49 | fix, 50 | }); 51 | } 52 | } 53 | }; 54 | }, 55 | }; 56 | -------------------------------------------------------------------------------- /.ci/eslint-plugin-zotero-translator/lib/rules/header-translator-id.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | const uuid = require('uuid/v4'); 6 | 7 | const { repo, parsed, header, IDconflict } = require('../../processor').support; 8 | 9 | const deleted = new Set( 10 | fs.readFileSync(path.join(repo, 'deleted.txt'), 'utf-8') 11 | .split('\n') 12 | .map(line => line.split(' ')[0]) 13 | .filter(id => id && id.indexOf('-') > 0) 14 | ); 15 | 16 | module.exports = { 17 | meta: { 18 | type: 'problem', 19 | docs: { 20 | description: 'disallows translatorID re-use', 21 | category: 'Potential Problems', 22 | }, 23 | fixable: 'code', 24 | }, 25 | 26 | create: function (context) { 27 | return { 28 | Program: function (node) { 29 | const filename = context.getFilename(); 30 | const translator = parsed(filename); 31 | if (!translator || !translator.header.fields) return; // regular js source, or header is invalid 32 | 33 | const translatorID = header(node).properties.find(p => p.key.value === 'translatorID'); 34 | 35 | if (!translatorID || !translatorID.value.value) { 36 | context.report({ 37 | node: header(node), 38 | message: 'Header has no translator ID', 39 | }); 40 | return; 41 | } 42 | 43 | if (deleted.has(translatorID.value.value)) { 44 | context.report({ 45 | node: translatorID.value, 46 | message: 'Header re-uses translator ID of deleted translator', 47 | fix: function (fixer) { 48 | return fixer.replaceText(translatorID.value, `"${uuid()}"`); 49 | } 50 | }); 51 | return; 52 | } 53 | 54 | const conflict = IDconflict(filename); 55 | if (conflict) { 56 | context.report({ 57 | node: translatorID.value, 58 | message: `re-uses translator ID of ${conflict.label}`, 59 | fix: fixer => fixer.replaceText(translatorID.value, `"${uuid()}"`), 60 | }); 61 | } 62 | } 63 | }; 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /.ci/eslint-plugin-zotero-translator/lib/rules/header-translator-type.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { parsed } = require('../../processor').support; 4 | 5 | module.exports = { 6 | meta: { 7 | type: 'problem', 8 | docs: { 9 | description: 'enforce translatorType against handler functions', 10 | category: 'Possible Errors', 11 | }, 12 | }, 13 | 14 | create: function (context) { 15 | return { 16 | Program: function (program) { 17 | const translator = parsed(context.getFilename()); 18 | if (!translator || !translator.header.fields) return; // regular js source, or header is invalid 19 | 20 | const functions = program.body.map((node) => { 21 | if (node.type === 'FunctionDeclaration') return node.id && node.id.name; 22 | if (node.type === 'VariableDeclaration' 23 | && node.declarations.length === 1 24 | && node.declarations[0].init 25 | && node.declarations[0].init.type === 'FunctionExpression') { 26 | return node.declarations[0].id.name; 27 | } 28 | return null; 29 | }) 30 | .filter(name => name); 31 | 32 | const type = { 33 | import: 1, 34 | export: 2, 35 | web: 4, 36 | search: 8 37 | }; 38 | 39 | const translatorType = translator.header.fields.translatorType; 40 | const browserSupport = translator.header.fields.browserSupport; 41 | 42 | if (browserSupport && !(translatorType & type.web)) { 43 | context.report({ 44 | loc: { start: { line: 1, column: 1 } }, 45 | message: `browserSupport set, but translatorType (${translatorType}) does not include web (${type.web})`, 46 | }); 47 | return; 48 | } 49 | 50 | for (const name of ['detectWeb', 'doWeb', 'detectImport', 'doImport', 'doExport', 'detectSearch', 'doSearch']) { 51 | const handler = functions.includes(name); 52 | const mode = name.replace(/^(detect|do)/, '').toLowerCase(); 53 | const bit = type[mode]; 54 | if (handler && !(translatorType & bit)) { 55 | context.report({ 56 | loc: { start: { line: 1, column: 1 } }, 57 | message: `${name} present, but translatorType (${translatorType}) does not specify ${mode} (${bit})`, 58 | }); 59 | return; 60 | } 61 | if (!handler && (translatorType & bit)) { 62 | let message = `translatorType specifies ${mode} (${bit}), but no ${name} present`; 63 | if (translatorType & type.web && mode !== 'web') { 64 | // Lots of common errors involve web translator developers not understanding 65 | // translator type jargon and checking too many boxes - checking "search" 66 | // because the translator supports search pages, or "import" because it 67 | // imports items from the site. 68 | // Be extra explicit when it seems like that might be the situation. 69 | message += `. This web translator is probably NOT a${bit <= 2 ? 'n' : ''} ${mode} translator, ` 70 | + `even if it supports "${mode}" pages or "${mode}ing". Uncheck "${mode}" in Scaffold.`; 71 | } 72 | context.report({ 73 | loc: { start: { line: 1, column: 1 } }, 74 | message, 75 | }); 76 | return; 77 | } 78 | } 79 | } 80 | }; 81 | }, 82 | }; 83 | -------------------------------------------------------------------------------- /.ci/eslint-plugin-zotero-translator/lib/rules/header-valid-json.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { parsed, json } = require('../../processor').support; 4 | 5 | module.exports = { 6 | meta: { 7 | type: 'problem', 8 | docs: { 9 | description: 'disallow invalid JSON in header', 10 | category: 'Possible Errors', 11 | }, 12 | }, 13 | 14 | create: function (context) { 15 | return { 16 | Program: function (_node) { 17 | const translator = parsed(context.getFilename()); 18 | 19 | if (!translator || translator.header.fields) return; // regular js source, or header is valid json 20 | 21 | const err = json.try(translator.header.text, { line: 0, position: 1 }); 22 | if (err) { 23 | context.report({ 24 | message: `Could not parse header: ${err.message}`, 25 | loc: { start: { line: err.line, column: err.column } }, 26 | }); 27 | } 28 | } 29 | }; 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /.ci/eslint-plugin-zotero-translator/lib/rules/license.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { parsed } = require('../../processor').support; 4 | const findRoot = require("find-root"); 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | 8 | module.exports = { 9 | meta: { 10 | type: 'problem', 11 | fixable: 'code', 12 | docs: { 13 | description: 'checks for AGPL license', 14 | category: 'Possible Errors', 15 | }, 16 | }, 17 | 18 | create: function (context) { 19 | return { 20 | Program: function (node) { 21 | const translator = parsed(context.getFilename()); 22 | 23 | if (!translator) return; // regular js source 24 | 25 | if (node.body.length < 2) return; // no body? 26 | 27 | const options = context.options[0]; 28 | if (!options.mustMatch) throw new Error('license/mustMatch not set'); 29 | if (!options.templateFile) throw new Error('license/templateFile not set'); 30 | 31 | const license = context.getSourceCode().getAllComments().find((comment) => { 32 | return comment.type === 'Block' && comment.value.match(/(BEGIN LICENSE BLOCK[\s\S]+END LICENSE BLOCK)|(Copyright)/i); 33 | }); 34 | 35 | if (!license) { 36 | const properties = translator.header.fields; 37 | const copyright = { 38 | holder: properties.creator || 'Zotero Contributors', 39 | period: `${(new Date).getFullYear()}`, 40 | }; 41 | if (properties.lastUpdated) { 42 | const year = properties.lastUpdated.split('-')[0] || ''; 43 | if (year && year !== copyright.period) copyright.period = `${year}-${copyright.period}`; 44 | } 45 | 46 | const templateFile = fs.existsSync(options.templateFile) 47 | ? options.templateFile 48 | : path.resolve(path.join(findRoot(context.getFilename()), options.templateFile)); 49 | if (!fs.existsSync(templateFile)) throw new Error(`cannot find ${templateFile}`); 50 | const template = fs.readFileSync(templateFile, 'utf-8'); 51 | 52 | const licenseText = '\n\n' + template.trim().replace(/\${(.*?)\}/g, (_, id) => { 53 | id = id.trim(); 54 | return copyright[id] || ``; 55 | }) + '\n\n'; 56 | context.report({ 57 | message: 'Missing license block', 58 | loc: node.body[1].loc.start, 59 | fix: fixer => fixer.insertTextBefore(node.body[1], licenseText), 60 | }); 61 | return; 62 | } 63 | 64 | if (node.body.length > 2 && node.body[1].loc.start.line < license.loc.start.line) { 65 | context.report({ 66 | loc: license.loc, 67 | message: 'Preferred to have license block at the top' 68 | }); 69 | return; 70 | } 71 | 72 | if (!license.value.match(new RegExp(options.mustMatch))) { 73 | context.report({ 74 | loc: license.loc, 75 | message: `Copyright preferred to be ${options.mustMatch}`, 76 | }); 77 | } 78 | } 79 | }; 80 | }, 81 | }; 82 | -------------------------------------------------------------------------------- /.ci/eslint-plugin-zotero-translator/lib/rules/no-for-each.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // this is a very simplistic rule to find 'for each' until I find a better eslint plugin that does this 4 | module.exports = { 5 | meta: { 6 | type: 'problem', 7 | docs: { 8 | description: 'disallow deprecated "for each"', 9 | category: 'Possible Errors', 10 | }, 11 | }, 12 | 13 | create: function (context) { 14 | return { 15 | Program: function (_node) { 16 | let lineno = 0; 17 | for (const line of context.getSourceCode().getText().split('\n')) { 18 | lineno += 1; 19 | 20 | const m = line.match(/for each *\(/); 21 | if (m) { 22 | context.report({ 23 | message: "Deprecated JavaScript 'for each' statement", 24 | loc: { start: { line: lineno, column: line.indexOf(m[0]) + 1 } }, 25 | }); 26 | } 27 | } 28 | } 29 | }; 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /.ci/eslint-plugin-zotero-translator/lib/rules/not-executable.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | const { parsed } = require('../../processor').support; 7 | 8 | module.exports = { 9 | meta: { 10 | type: 'suggestion', 11 | docs: { 12 | description: 'disallow executable status on translators', 13 | category: 'Best Practices', 14 | }, 15 | }, 16 | 17 | create: function (context) { 18 | return { 19 | Program: function (node) { 20 | if (process.platform == 'win32') return; // X_OK always succeeds on Windows 21 | 22 | const filename = context.getFilename(); 23 | if (!parsed(filename)) return; // only check translators 24 | 25 | try { 26 | fs.accessSync(filename, fs.constants.X_OK); 27 | context.report(node, `Translator '${path.basename(filename)}' should not be executable.`); 28 | } 29 | catch (err) { 30 | } 31 | } 32 | }; 33 | }, 34 | }; 35 | -------------------------------------------------------------------------------- /.ci/eslint-plugin-zotero-translator/lib/rules/prefer-index-of.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | meta: { 5 | type: 'suggestion', 6 | docs: { 7 | description: 'suggest alternative to unnecessary use of indexOf or search', 8 | category: 'Stylistic Issues', 9 | }, 10 | fixable: 'code', 11 | }, 12 | 13 | create: function (context) { 14 | return { 15 | "BinaryExpression > CallExpression:matches([callee.property.name='indexOf'], [callee.property.name='search'])[arguments.length=1]": (node) => { 16 | let source = context.getSourceCode(); 17 | let binary = node.parent; 18 | if ( 19 | (binary.operator.startsWith('==') 20 | || binary.operator.startsWith('!=') 21 | || binary.operator === '>') && source.getText(binary.right) === '-1' 22 | || (binary.operator === '<' || binary.operator === '>=') && source.getText(binary.right) === '0' 23 | ) { 24 | context.report({ 25 | node, 26 | message: node.callee.property.name === 'indexOf' 27 | ? "Unnecessary '.indexOf()', use '.includes()' instead" 28 | : "Unnecessary '.search()', use 'RegExp#test()' instead", 29 | * fix(fixer) { 30 | let test = node.callee.property.name === 'indexOf' 31 | ? `${source.getText(node.callee.object)}.contains(${source.getText(node.arguments[0])})` 32 | : `${source.getText(node.arguments[0])}.test(${source.getText(node.callee.object)})`; 33 | let positiveMatch = binary.operator.startsWith('!=') 34 | || binary.operator === '>' 35 | || binary.operator === '>='; 36 | if (!positiveMatch) { 37 | // This might produce unnecessary parens, but unfortunately it's the best we can do 38 | test = `!(${test})`; 39 | } 40 | yield fixer.replaceText(binary, test); 41 | } 42 | }); 43 | } 44 | } 45 | }; 46 | }, 47 | }; 48 | -------------------------------------------------------------------------------- /.ci/eslint-plugin-zotero-translator/lib/rules/robust-query-selector.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | meta: { 5 | type: 'suggest', 6 | docs: { 7 | description: 'suggest alternatives to brittle querySelector() strings', 8 | }, 9 | fixable: 'code', 10 | }, 11 | 12 | create: function (context) { 13 | return { 14 | "CallExpression:matches([callee.property.name=/querySelector(All)?/], [callee.name=/attr|text|innerText/])[arguments.0.type=Literal]": (node) => { 15 | let arg = node.arguments[0].raw; 16 | if (typeof arg !== 'string') { 17 | return; 18 | } 19 | let idRe = /\[id=(["'])([^"'.#\s]+)\1]/g; 20 | if (idRe.test(arg)) { 21 | context.report({ 22 | node, 23 | message: "Prefer #id over [id=\"id\"]", 24 | * fix(fixer) { 25 | yield fixer.replaceText(node.arguments[0], arg.replaceAll(idRe, "#$2")); 26 | } 27 | }); 28 | } 29 | let classRe = /\[class=(["'])([^"'.#]+)\1]/g; 30 | if (classRe.test(arg)) { 31 | context.report({ 32 | node, 33 | message: "Prefer .class over [class=\"class\"]", 34 | * fix(fixer) { 35 | yield fixer.replaceText(node.arguments[0], 36 | arg.replaceAll(classRe, (_, __, name) => `.${name.replaceAll(/\s+/g, '.')}`)); 37 | } 38 | }); 39 | } 40 | } 41 | }; 42 | }, 43 | }; 44 | -------------------------------------------------------------------------------- /.ci/eslint-plugin-zotero-translator/lib/rules/test-cases.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { parsed, json } = require('../../processor').support; 4 | 5 | function zip(arrays) { 6 | let zipped = null; 7 | for (const [key, array] of Object.entries(arrays)) { 8 | if (!zipped) { 9 | zipped = Array(array.length).fill(null).map((_, i) => ({ _: i })); 10 | } 11 | else if (array.length !== zipped.length) { 12 | throw new Error(`Array length mismatch: ${key} has ${array.length} elements, but ${zipped.length} expected`); 13 | } 14 | for (const [i, value] of array.entries()) { 15 | zipped[i][key] = value; 16 | } 17 | } 18 | return zipped; 19 | } 20 | 21 | module.exports = { 22 | meta: { 23 | type: 'problem', 24 | 25 | docs: { 26 | description: 'disallow invalid tests', 27 | category: 'Possible Errors', 28 | }, 29 | }, 30 | 31 | create: function (context) { 32 | return { 33 | Program: function (node) { 34 | const translator = parsed(context.getFilename()); 35 | if (!translator || !translator.testcases.text) return; // regular js source, or no testcases 36 | 37 | const err = json.try(translator.testcases.text, { line: translator.testcases.start, position: 3 }); 38 | if (err) { 39 | context.report({ 40 | message: `Could not parse testcases: ${err.message}`, 41 | loc: { start: { line: err.line, column: err.column } }, 42 | }); 43 | return; 44 | } 45 | 46 | const declaration = node.body.find(node => ( 47 | node.type === 'VariableDeclaration' 48 | && node.declarations.length === 1 49 | && node.declarations[0].id.name === 'testCases' 50 | )); 51 | if (declaration.followingStatement) { 52 | context.report({ 53 | node: declaration.followingStatement, 54 | message: 'testCases should not have trailing code', 55 | }); 56 | } 57 | 58 | const nodes = declaration.declarations[0].init.elements; 59 | if (!Array.isArray(nodes)) { 60 | context.report({ 61 | node: declaration, 62 | message: 'testCases must be an array', 63 | }); 64 | return; 65 | } 66 | 67 | const sourceCode = context.getSourceCode(); 68 | const token = sourceCode.getLastToken(node); 69 | if (token.type === 'Punctuator' && token.value === ';') { 70 | context.report({ 71 | message: 'testCases should not have trailing semicolon', 72 | loc: declaration.loc.end, 73 | }); 74 | } 75 | 76 | zip({ 77 | testCase: JSON.parse(translator.testcases.text), 78 | node: nodes, 79 | }) 80 | .forEach(({ testCase, node }) => { 81 | if (!['web', 'import', 'search'].includes(testCase.type)) { 82 | context.report({ 83 | node, 84 | message: `test case has invalid type "${testCase.type}"`, 85 | }); 86 | return; 87 | } 88 | 89 | if (!(Array.isArray(testCase.items) || (testCase.type === 'web' && testCase.items === 'multiple'))) { 90 | context.report({ 91 | node, 92 | message: `test case of type "${testCase.type}" needs items`, 93 | }); 94 | } 95 | 96 | if (testCase.type === 'web' && typeof testCase.url !== 'string') { 97 | context.report({ 98 | node, 99 | message: `test case of type "${testCase.type}" test needs url`, 100 | }); 101 | } 102 | 103 | if (['import', 'search'].includes(testCase.type) && !testCase.input) { 104 | context.report({ 105 | node, 106 | message: `test case of type "${testCase.type}" needs a string input`, 107 | }); 108 | } 109 | else if (testCase.type === 'import' && typeof testCase.input !== 'string') { 110 | context.report({ 111 | node, 112 | message: `test case of type "${testCase.type}" needs input`, 113 | }); 114 | } 115 | else if (testCase.type === 'search') { 116 | // console.log(JSON.stringify(testCase.input)) 117 | const expected = ['DOI', 'ISBN', 'PMID', 'arXiv', 'identifiers', 'contextObject', 'adsBibcode', 'ericNumber', 'openAlex']; 118 | let keys; 119 | if (Array.isArray(testCase.input)) { 120 | keys = testCase.input.flatMap(Object.keys); 121 | } 122 | else { 123 | keys = Object.keys(testCase.input); 124 | } 125 | if (!keys.every(key => expected.includes(key))) { 126 | const invalidKey = keys.find(key => !expected.includes(key)); 127 | context.report({ 128 | node, 129 | message: `test case of type "${testCase.type}" has invalid search term '${invalidKey}' - expected one of ${expected.join(', ')}`, 130 | }); 131 | } 132 | } 133 | 134 | if (Array.isArray(testCase.items)) { 135 | zip({ 136 | item: testCase.items, 137 | node: node.properties.find(prop => prop.key.type === 'Literal' && prop.key.value === 'items').value.elements, 138 | }) 139 | .forEach(({ item, node }) => { 140 | if (!Array.isArray(item.creators)) { 141 | context.report({ 142 | message: 'creators should be an array', 143 | node, 144 | }); 145 | return; 146 | } 147 | 148 | zip({ 149 | creator: item.creators, 150 | node: node.properties.find(prop => prop.key.type === 'Literal' && prop.key.value === 'creators').value.elements, 151 | }) 152 | .forEach(({ creator, node }) => { 153 | if (creator.fieldMode !== undefined && creator.fieldMode !== 1) { 154 | context.report({ 155 | node, 156 | message: 'creator.fieldMode should be omitted or 1', 157 | }); 158 | } 159 | else if (creator.fieldMode === 1 && (creator.firstName || !creator.lastName)) { 160 | context.report({ 161 | node, 162 | message: 'creator with fieldMode == 1 should have lastName and no firstName', 163 | }); 164 | } 165 | else if (!creator.firstName && !creator.lastName) { 166 | context.report({ 167 | node, 168 | message: 'creator has no name', 169 | }); 170 | } 171 | 172 | if (!creator.creatorType) { 173 | context.report({ 174 | node, 175 | message: 'creator has no creatorType', 176 | }); 177 | } 178 | }); 179 | }); 180 | } 181 | }); 182 | } 183 | }; 184 | }, 185 | }; 186 | -------------------------------------------------------------------------------- /.ci/eslint-plugin-zotero-translator/lib/rules/translator-framework.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const { parsed } = require('../../processor').support; 4 | 5 | module.exports = { 6 | meta: { 7 | type: 'problem', 8 | fixable: 'code', 9 | docs: { 10 | description: 'checks use of deprecated Translator Framework', 11 | category: 'Possible Errors', 12 | }, 13 | }, 14 | 15 | create: function (context) { 16 | return { 17 | Program: function (_node) { 18 | const translator = parsed(context.getFilename()); 19 | if (!translator || !translator.FW) return; // regular js source, or no FW present 20 | 21 | context.report({ 22 | loc: translator.FW.loc, 23 | message: 'uses deprecated Translator Framework' 24 | }); 25 | } 26 | }; 27 | }, 28 | }; 29 | -------------------------------------------------------------------------------- /.ci/eslint-plugin-zotero-translator/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-zotero-translator", 3 | "version": "0.0.1", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "eslint-plugin-zotero-translator", 9 | "version": "0.0.1", 10 | "license": "ISC", 11 | "dependencies": { 12 | "commander": "^2.19.0", 13 | "find-root": "^1.1.0", 14 | "recursive-readdir": "^2.2.3", 15 | "require-dir": "^1.2.0", 16 | "uuid": "^3.3.2" 17 | }, 18 | "bin": { 19 | "teslint": "bin/teslint.js" 20 | }, 21 | "engines": { 22 | "node": ">=16.0.0" 23 | } 24 | }, 25 | "node_modules/balanced-match": { 26 | "version": "1.0.2", 27 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 28 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" 29 | }, 30 | "node_modules/brace-expansion": { 31 | "version": "1.1.11", 32 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 33 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 34 | "dependencies": { 35 | "balanced-match": "^1.0.0", 36 | "concat-map": "0.0.1" 37 | } 38 | }, 39 | "node_modules/commander": { 40 | "version": "2.20.3", 41 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", 42 | "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" 43 | }, 44 | "node_modules/concat-map": { 45 | "version": "0.0.1", 46 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 47 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" 48 | }, 49 | "node_modules/find-root": { 50 | "version": "1.1.0", 51 | "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", 52 | "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" 53 | }, 54 | "node_modules/minimatch": { 55 | "version": "3.1.2", 56 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 57 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 58 | "dependencies": { 59 | "brace-expansion": "^1.1.7" 60 | }, 61 | "engines": { 62 | "node": "*" 63 | } 64 | }, 65 | "node_modules/recursive-readdir": { 66 | "version": "2.2.3", 67 | "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", 68 | "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==", 69 | "dependencies": { 70 | "minimatch": "^3.0.5" 71 | }, 72 | "engines": { 73 | "node": ">=6.0.0" 74 | } 75 | }, 76 | "node_modules/require-dir": { 77 | "version": "1.2.0", 78 | "resolved": "https://registry.npmjs.org/require-dir/-/require-dir-1.2.0.tgz", 79 | "integrity": "sha512-LY85DTSu+heYgDqq/mK+7zFHWkttVNRXC9NKcKGyuGLdlsfbjEPrIEYdCVrx6hqnJb+xSu3Lzaoo8VnmOhhjNA==", 80 | "engines": { 81 | "node": "*" 82 | } 83 | }, 84 | "node_modules/uuid": { 85 | "version": "3.4.0", 86 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", 87 | "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", 88 | "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", 89 | "bin": { 90 | "uuid": "bin/uuid" 91 | } 92 | } 93 | }, 94 | "dependencies": { 95 | "balanced-match": { 96 | "version": "1.0.2", 97 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 98 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" 99 | }, 100 | "brace-expansion": { 101 | "version": "1.1.11", 102 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", 103 | "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", 104 | "requires": { 105 | "balanced-match": "^1.0.0", 106 | "concat-map": "0.0.1" 107 | } 108 | }, 109 | "commander": { 110 | "version": "2.20.3", 111 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", 112 | "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" 113 | }, 114 | "concat-map": { 115 | "version": "0.0.1", 116 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 117 | "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" 118 | }, 119 | "find-root": { 120 | "version": "1.1.0", 121 | "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", 122 | "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" 123 | }, 124 | "minimatch": { 125 | "version": "3.1.2", 126 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", 127 | "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", 128 | "requires": { 129 | "brace-expansion": "^1.1.7" 130 | } 131 | }, 132 | "recursive-readdir": { 133 | "version": "2.2.3", 134 | "resolved": "https://registry.npmjs.org/recursive-readdir/-/recursive-readdir-2.2.3.tgz", 135 | "integrity": "sha512-8HrF5ZsXk5FAH9dgsx3BlUer73nIhuj+9OrQwEbLTPOBzGkL1lsFCR01am+v+0m2Cmbs1nP12hLDl5FA7EszKA==", 136 | "requires": { 137 | "minimatch": "^3.0.5" 138 | } 139 | }, 140 | "require-dir": { 141 | "version": "1.2.0", 142 | "resolved": "https://registry.npmjs.org/require-dir/-/require-dir-1.2.0.tgz", 143 | "integrity": "sha512-LY85DTSu+heYgDqq/mK+7zFHWkttVNRXC9NKcKGyuGLdlsfbjEPrIEYdCVrx6hqnJb+xSu3Lzaoo8VnmOhhjNA==" 144 | }, 145 | "uuid": { 146 | "version": "3.4.0", 147 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", 148 | "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" 149 | } 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /.ci/eslint-plugin-zotero-translator/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-plugin-zotero-translator", 3 | "version": "0.0.1", 4 | "description": "Checks Zotero translators for errors and recommended style", 5 | "author": "Emiliano Heyns", 6 | "main": "index.js", 7 | "engines": { 8 | "node": ">=16.0.0" 9 | }, 10 | "license": "ISC", 11 | "bin": { 12 | "teslint": "./bin/teslint.js" 13 | }, 14 | "dependencies": { 15 | "commander": "^2.19.0", 16 | "find-root": "^1.1.0", 17 | "recursive-readdir": "^2.2.3", 18 | "require-dir": "^1.2.0", 19 | "uuid": "^3.3.2" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /.ci/eslint-plugin-zotero-translator/processor.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const espree = require('espree'); 4 | const clarinet = require('clarinet'); 5 | const findRoot = require('find-root'); 6 | const path = require('path'); 7 | const fs = require('fs'); 8 | const childProcess = require('child_process'); 9 | 10 | let repo; 11 | try { 12 | repo = path.resolve(findRoot(__dirname, dir => fs.existsSync(path.join(dir, '.git')))); 13 | } 14 | catch (e) { 15 | console.error('ERROR: Translators can only be linted inside a clone of the zotero/translators repo (not a ZIP downloaded from GitHub)'); 16 | console.error(' git clone https://github.com/zotero/translators.git'); 17 | process.exit(1); // eslint-disable-line no-process-exit 18 | } 19 | 20 | function exec(cmd) { 21 | return childProcess.execSync(cmd, { cwd: repo, encoding: 'utf8' }); 22 | } 23 | 24 | // have to pre-load everything to test for conflicting headers 25 | const cache = new Map(); 26 | 27 | function updateCache(text, filename) { 28 | if (text[0] !== '{') return; 29 | if (cache.has(filename) && cache.get(filename).text === text) { 30 | // No change - no need to re-parse 31 | return; 32 | } 33 | 34 | // detect header 35 | const prefix = `const ZoteroTranslator${Date.now()} = `; 36 | const decorated = `${prefix}${text}`; 37 | let ast; 38 | try { 39 | ast = espree.parse(decorated, { comment: true, loc: true, ecmaVersion: 2023 }); 40 | } 41 | catch (err) { 42 | console.log(filename, err.message); 43 | process.exit(1); // eslint-disable-line no-process-exit 44 | } 45 | 46 | const header = ((ast.body[0] || {}).declarations[0] || {}).init; 47 | const testcases = ast.body 48 | .filter((node, i) => i === ast.body.length - 1) 49 | .filter(node => node.type === 'VariableDeclaration' && node.declarations.length === 1).map(node => node.declarations[0]) 50 | .filter(node => node.type === 'VariableDeclarator' && node.id.type === 'Identifier' && node.id.name === 'testCases') 51 | .map(node => node.init)[0]; 52 | 53 | const extract = (node) => { 54 | if (!node) return {}; 55 | return { 56 | start: node.loc.start.line, 57 | end: node.loc.end.line, 58 | text: decorated.substring(node.start, node.end), 59 | }; 60 | }; 61 | 62 | const entry = { 63 | text, 64 | header: extract(header), 65 | testcases: extract(testcases), 66 | FW: ast.comments.find(comment => comment.type === 'Block' && comment.value.trim === 'FW LINE 59:b820c6d') 67 | }; 68 | 69 | try { 70 | entry.header.fields = JSON.parse(entry.header.text); 71 | } 72 | catch (err) { 73 | // ignore 74 | } 75 | 76 | 77 | cache.set(filename, entry); 78 | } 79 | 80 | for (let filename of fs.readdirSync(repo).sort()) { 81 | if (!filename.endsWith('.js')) continue; 82 | filename = path.join(repo, filename); 83 | 84 | const text = fs.readFileSync(filename, 'utf-8'); 85 | updateCache(text, filename); 86 | } 87 | 88 | for (const lu of exec(`git grep '"lastUpdated"' HEAD~1`).split('\n')) { 89 | const m = lu.match(/^HEAD~1:([^:]+):\s*"lastUpdated"\s*:\s*"([-0-9: ]+)"/); 90 | if (!m) continue; 91 | const [, translator, lastUpdated] = m; 92 | const filename = path.join(repo, translator); 93 | if (cache.has(filename)) cache.get(filename).lastUpdated = lastUpdated; 94 | } 95 | 96 | function tryJSON(json, offset) { 97 | const parser = clarinet.parser(); 98 | let error; 99 | 100 | const message = e => ({ 101 | message: (e.message || '').split('\n', 1)[0], 102 | line: parser.line + offset.line, 103 | column: parser.column, 104 | position: parser.position + offset.position, 105 | }); 106 | 107 | // trigger the parse error 108 | parser.onerror = function (e) { 109 | error = message(e); 110 | parser.close(); 111 | }; 112 | 113 | try { 114 | parser.write(json).close(); 115 | } 116 | catch (e) { 117 | return error || message(e); 118 | } 119 | 120 | return error; 121 | } 122 | 123 | function JSONTokens(json, offset) { 124 | const parser = clarinet.parser(); 125 | const tokens = []; 126 | 127 | parser.onvalue = function (v) { 128 | tokens.push({ type: 'value', value: v, line: parser.line + offset.line, column: parser.column, position: parser.position + offset.position }); 129 | }; 130 | parser.onopenobject = function (key) { 131 | tokens.push({ type: 'object-open', key, line: parser.line + offset.line, column: parser.column, position: parser.position + offset.position }); 132 | }; 133 | parser.onkey = function (key) { 134 | tokens.push({ type: 'key', key, line: parser.line + offset.line, column: parser.column, position: parser.position + offset.position }); 135 | }; 136 | parser.oncloseobject = function () { 137 | tokens.push({ type: 'object-close', line: parser.line + offset.line, column: parser.column, position: parser.position + offset.position }); 138 | }; 139 | parser.onopenarray = function () { 140 | tokens.push({ type: 'array-open', line: parser.line + offset.line, column: parser.column, position: parser.position + offset.position }); 141 | }; 142 | parser.onclosearray = function () { 143 | tokens.push({ type: 'array-close', line: parser.line + offset.line, column: parser.column, position: parser.position + offset.position }); 144 | }; 145 | 146 | parser.write(json).close(); 147 | return tokens; 148 | } 149 | 150 | function header(program) { 151 | if (!program) return null; 152 | if (program.type !== 'Program') return null; 153 | if (program.body.length === 0) return null; 154 | if (program.body[0].type !== 'ExpressionStatement') return null; 155 | if (program.body[0].expression.type !== 'ObjectExpression') return null; 156 | return program.body[0].expression; 157 | } 158 | 159 | function conflict(filename) { 160 | const translatorID = (((cache.get(filename) || {}).header || {}).fields || {}).translatorID; 161 | if (!translatorID) return null; 162 | for (const [other, header] of cache.entries()) { 163 | if (other !== filename && header.translatorID === translatorID) { 164 | return header.fields; 165 | } 166 | } 167 | return null; 168 | } 169 | 170 | const junk = new RegExp(`${path.sep}0_`.replace(/[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&') + '.*'); 171 | module.exports = { 172 | support: { 173 | repo, 174 | parsed: filename => cache.get(filename.replace(junk, '')), 175 | header, 176 | IDconflict: conflict, 177 | json: { 178 | try: tryJSON, 179 | tokens: JSONTokens, 180 | } 181 | }, 182 | 183 | supportsAutofix: true, 184 | 185 | preprocess: function (text, filename) { 186 | // We might be running on an in-memory version of the translator newer 187 | // than what we read from disk earlier, so update the cache 188 | updateCache(text, filename); 189 | 190 | const parsed = cache.get(filename); 191 | if (text[0] !== '{' || !parsed) return [{ text, filename }]; 192 | 193 | if (parsed.header.text) { 194 | return [{ text: `(${text.slice(0, parsed.header.text.length)});${text.slice(parsed.header.text.length)}`, filename }]; 195 | } 196 | else { 197 | return [{ text, filename }]; 198 | } 199 | }, 200 | 201 | postprocess: function (messages, filename) { 202 | messages = [].concat(...messages); 203 | 204 | const parsed = cache.get(filename); 205 | 206 | if (parsed) { 207 | const header = parsed.header; 208 | if (header.text) { 209 | messages = messages.filter((m) => { 210 | if (!m.ruleId) return true; 211 | if (m.ruleId.startsWith('zotero-translator/header') && m.line > header.end) return false; 212 | switch (m.ruleId) { 213 | case 'no-unused-expressions': 214 | return m.line !== 1; 215 | case 'quote-props': 216 | return m.line > header.end; 217 | default: 218 | } 219 | return true; 220 | }); 221 | 222 | const adjust = (p) => { 223 | if (p > header.text.length) return p - 3; // remove '(' and ');' 224 | if (p > 1) return p - 1; // remove '(' 225 | return p; 226 | }; 227 | for (const m of messages) { 228 | if (m.fix) m.fix.range = m.fix.range.map(adjust); 229 | if (m.suggestions) { 230 | for (const s of m.suggestions) { 231 | if (s.fix) s.fix.range = s.fix.range.map(adjust); 232 | } 233 | } 234 | } 235 | } 236 | 237 | const testcases = parsed.testcases; 238 | if (testcases && testcases.text) { 239 | messages = messages.filter((m) => { 240 | if (!m.ruleId) return true; 241 | if (m.ruleId.startsWith('zotero-translator/test-cases') && m.line < testcases.start) return false; 242 | 243 | switch (m.ruleId) { 244 | case 'semi': 245 | case 'quote-props': 246 | return m.line < testcases.start || m.line > testcases.end; 247 | case 'lines-around-comment': 248 | return m.line !== testcases.end + 1; 249 | } 250 | return true; 251 | }); 252 | } 253 | } 254 | 255 | return messages; 256 | }, 257 | }; 258 | -------------------------------------------------------------------------------- /.ci/helper.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [[ -t 1 && "$(tput colors)" -gt 0 ]]; then 4 | export color_ok=$'\e[32;1m' 5 | export color_notok=$'\e[31;1m' 6 | export color_warn=$'\e[33m' 7 | export color_err=$'\e[31m' 8 | export color_reset=$'\e[0m' 9 | fi 10 | 11 | get_translator_id() { 12 | if [[ -n "$1" ]];then 13 | grep -r '"translatorID"' "$@" | sed -e 's/[" ,]//g' -e 's/^.*://g' 14 | else 15 | while read line;do 16 | echo "$line"|grep '"translatorID"' | sed -e 's/[" ,]//g' -e 's/^.*://g' 17 | done 18 | fi 19 | } 20 | 21 | get_translators_to_check() { 22 | # If a PR branch has no conflicts with the master then git 23 | # creates a custom merge commit where it merges PR into master. 24 | # Travis-CI tests on that commit instead of the HEAD of the PR branch. 25 | # 26 | # Thus below we first determine if HEAD is a merge commit by checking how 27 | # many parents the current HEAD has. If number of parents > 1, then it's a merge commit 28 | # in which case we need to diff translator names between HEAD^2 and PR split commit from master. 29 | # The above will generally only be the case in CI or if using a custom PR pulling script which 30 | # pulls the merge PR commit instead of just the PR branch. 31 | # 32 | # If the HEAD commit is not a merge then we diff HEAD with PR split commit from master. This is the case 33 | # when running from a local development PR branch 34 | # 35 | # The branching point hash retrieval logic is based on https://stackoverflow.com/a/12185115/3199106 36 | # 37 | # We get the set of modified files with git diff, passing --diff-filter=d to exclude deleted files. 38 | 39 | TRANSLATORS_TO_CHECK="" 40 | 41 | # Push to master 42 | if [ "${GITHUB_REF:-}" = "refs/heads/master" ]; then 43 | before_commit=$(jq -r '.before' $(echo $GITHUB_EVENT_PATH)) 44 | TRANSLATORS_TO_CHECK=$(git diff $before_commit --name-only --diff-filter=d | { grep -e "^[^/]*.js$" || true; }) 45 | # Pull request 46 | else 47 | # Gets parent commits. Either one or two hashes 48 | parent_commits=($(git show --no-patch --format="%P" HEAD)) 49 | # Size of $parent_commits array 50 | num_parent_commits=${#parent_commits[@]} 51 | if [ $num_parent_commits -gt 1 ]; then 52 | first_parent=$(git rev-list --first-parent ^master HEAD^2 | tail -n1) 53 | branch_point=$(git rev-list "$first_parent^^!") 54 | TRANSLATORS_TO_CHECK=$(git diff HEAD^2 $branch_point --name-only --diff-filter=d | { grep -e "^[^/]*.js$" || true; }) 55 | else 56 | first_parent=$(git rev-list --first-parent ^master HEAD | tail -n1) 57 | branch_point=$(git rev-list "$first_parent^^!") 58 | TRANSLATORS_TO_CHECK=$(git diff $branch_point --name-only --diff-filter=d | { grep -e "^[^/]*.js$" || true; }) 59 | fi 60 | fi 61 | } 62 | -------------------------------------------------------------------------------- /.ci/lint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | dir="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 5 | 6 | . "$dir/helper.sh" 7 | 8 | get_translators_to_check 9 | if [ -n "$TRANSLATORS_TO_CHECK" ]; then 10 | # No `xargs -d` support in macOS, so workaround with `tr` 11 | echo "$TRANSLATORS_TO_CHECK" | tr '\n' '\0' | xargs -0 npm run lint -- 12 | fi 13 | -------------------------------------------------------------------------------- /.ci/pull-request-check/check-pull-request.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | 4 | echo "::group::Setup" 5 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 6 | ROOT_DIR="$( dirname "$DIR" )" 7 | 8 | . "$ROOT_DIR/helper.sh" 9 | 10 | # Build connector 11 | mkdir -p connectors 12 | cd connectors 13 | 14 | if [ -d .git ]; then 15 | # Temp fix for connectors/src/zotero/resource/schema/global submodule fetch failing 16 | git config url."https://".insteadOf git:// 17 | git pull 18 | git submodule update 19 | git -C src/zotero/ submodule update -- resource/schema/global 20 | git -C src/zotero submodule update -- resource/SingleFile 21 | npm ci 22 | else 23 | git clone https://github.com/zotero/zotero-connectors.git --depth 1 . 24 | git config url."https://".insteadOf git:// 25 | git submodule update --init --depth 1 26 | git -C src/zotero submodule update --init --depth 1 -- resource/schema/global 27 | git -C src/zotero submodule update --init --depth 1 -- resource/SingleFile 28 | npm ci 29 | fi 30 | 31 | export ZOTERO_REPOSITORY_URL="http://localhost:8085/" 32 | export CHROME_EXTENSION_KEY="MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDllBS5q+Z9T9tPgYwRN+/8T9wzyjo9tRo03Wy8zP2DQ5Iy+3q0Tjq2vKXGiMCxC/ZVuEMC68Ekv+jNT43VxPbEXI4dzpK1GMBqPJpAcEOB8B1ROBouQMbGGTG7fOdQVlmpdTTPVndVwysJ02CrDMn96IG2ytOq2PO7GR2xleCudQIDAQAB" 33 | ./build.sh -p b -d 34 | cd .. 35 | 36 | npm explore chromedriver -- npm run install --detect_chromedriver_version 37 | echo "::endgroup::" 38 | 39 | get_translators_to_check 40 | ./selenium-test.js "$TRANSLATORS_TO_CHECK" 41 | 42 | -------------------------------------------------------------------------------- /.ci/pull-request-check/selenium-test.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const path = require('path'); 4 | const process = require('process'); 5 | const selenium = require('selenium-webdriver'); 6 | const until = require('selenium-webdriver/lib/until'); 7 | const chalk = require('chalk'); 8 | 9 | const translatorServer = require('./translator-server'); 10 | 11 | const chromeExtensionDir = path.join(__dirname, 'connectors', 'build', 'chrome'); 12 | const KEEP_BROWSER_OPEN = 'KEEP_BROWSER_OPEN' in process.env; 13 | const ZOTERO_CONNECTOR_EXTENSION_ID = 'ekhagklcjbdpajgpjgmbionohlpdbjgc'; 14 | 15 | async function getTranslatorsToTest() { 16 | const translatorFilenames = process.argv[2].split('\n').filter(filename => filename.trim().length > 0); 17 | let changedTranslatorIDs = []; 18 | let toTestTranslatorIDs = new Set(); 19 | let toTestTranslatorNames = new Set(); 20 | for (const translatorFilename of translatorFilenames) { 21 | let translatorInfo = translatorServer.filenameToTranslator[translatorFilename].metadata; 22 | changedTranslatorIDs.push(translatorInfo.translatorID); 23 | toTestTranslatorIDs.add(translatorInfo.translatorID); 24 | toTestTranslatorNames.add(translatorInfo.label); 25 | } 26 | // Find all translators that use the changed translators and add them to list/check them too 27 | let tooManyTranslators = false; 28 | for (let translator of translatorServer.translators) { 29 | for (let translatorID of changedTranslatorIDs) { 30 | if (!translator.content.includes(translatorID)) continue; 31 | 32 | toTestTranslatorIDs.add(translator.metadata.translatorID); 33 | toTestTranslatorNames.add(translator.metadata.label); 34 | if (toTestTranslatorIDs.size >= 10) { 35 | tooManyTranslators = true; 36 | break; 37 | } 38 | } 39 | if (tooManyTranslators) break; 40 | } 41 | if (tooManyTranslators) { 42 | console.log( 43 | `Over 10 translators need to be tested, but this will take too long 44 | and timeout the CI environment. Truncating to 10. 45 | 46 | This is likely to happen when changing Embedded Metadata which is 47 | loaded by pretty much every other translator or when a PR contains 48 | a lot of changed translators. 49 | 50 | You may want to consider adding '[ci skip]' in the commit message.` 51 | ) 52 | } 53 | console.log(`Will run tests for translators ${JSON.stringify(Array.from(toTestTranslatorNames))}`); 54 | return Array.from(toTestTranslatorIDs); 55 | } 56 | 57 | function report(results) { 58 | var allPassed = true; 59 | for (let translatorID in results) { 60 | let translatorResults = results[translatorID]; 61 | console.log(chalk.bold(chalk.bgWhite(chalk.black(`Beginning Tests for ${translatorID}: ${translatorResults.label}`)))); 62 | let padding = 2; 63 | let output = translatorResults.message.split("\n"); 64 | for (let line of output) { 65 | if (line.match(/^TranslatorTester: Running .+ Test [0-9]*$/) || 66 | line.match(/^TranslatorTester: Running [0-9]* tests for .*$/)) { 67 | console.log(" ".repeat(padding-1) + chalk.bgCyan(chalk.black(line))); 68 | } 69 | else if (line.match(/^-/)) { 70 | console.log(chalk.red("-" + " ".repeat(padding) + line.substr(1))); 71 | } 72 | else if (line.match(/^\+/)) { 73 | console.log(chalk.green("+" + " ".repeat(padding) + line.substr(1))); 74 | } 75 | else if (line.match(/^TranslatorTester: .+ Test [0-9]*: succeeded/)) { 76 | console.log(" ".repeat(padding) + chalk.bgGreen(line)); 77 | } 78 | else if (line.match(/^TranslatorTester: .+ Test [0-9]*: unknown/)) { 79 | console.log(" ".repeat(padding) + chalk.bgYellow(chalk.black(line))); 80 | allPassed = false; 81 | } 82 | else if (line.match(/^TranslatorTester: .+ Test [0-9]*: failed/)) { 83 | console.log(" ".repeat(padding) + chalk.bgRed(line)); 84 | allPassed = false; 85 | } 86 | else { 87 | console.log(" ".repeat(padding) + line); 88 | } 89 | } 90 | console.log("\n"); 91 | } 92 | 93 | return allPassed 94 | } 95 | 96 | var allPassed = false; 97 | 98 | (async function() { 99 | let driver; 100 | try { 101 | await translatorServer.serve(); 102 | require('chromedriver'); 103 | let chrome = require('selenium-webdriver/chrome'); 104 | let options = new chrome.Options(); 105 | options.addArguments(`load-extension=${chromeExtensionDir}`); 106 | if ('BROWSER_EXECUTABLE' in process.env) { 107 | options.setChromeBinaryPath(process.env['BROWSER_EXECUTABLE']); 108 | } 109 | 110 | driver = new selenium.Builder() 111 | .forBrowser('chrome') 112 | .setChromeOptions(options) 113 | .build(); 114 | 115 | // We got the test URL, let's test 116 | const translatorsToTest = await getTranslatorsToTest(); 117 | let testUrl = `chrome-extension://${ZOTERO_CONNECTOR_EXTENSION_ID}/tools/testTranslators/testTranslators.html#translators=${translatorsToTest.join(',')}`; 118 | await new Promise((resolve) => setTimeout(() => resolve(driver.get(testUrl)), 500)); 119 | await driver.wait(until.elementLocated({id: 'translator-tests-complete'}), 30*60*1000); 120 | testResults = await driver.executeScript('return window.seleniumOutput'); 121 | 122 | allPassed = report(testResults); 123 | } 124 | catch (e) { 125 | console.error(e); 126 | } 127 | finally { 128 | if (!KEEP_BROWSER_OPEN) { 129 | await driver.quit(); 130 | } 131 | translatorServer.stopServing(); 132 | if (allPassed) { 133 | console.log(chalk.green("All translator tests passed")); 134 | } else { 135 | console.log(chalk.red("Some translator tests failed")); 136 | } 137 | process.exit(allPassed ? 0 : 1); 138 | } 139 | })(); 140 | -------------------------------------------------------------------------------- /.ci/pull-request-check/translator-server.js: -------------------------------------------------------------------------------- 1 | const http = require("http"); 2 | const fs = require('fs').promises; 3 | const path = require('path'); 4 | 5 | const host = 'localhost'; 6 | const port = 8085; 7 | 8 | var server; 9 | var translators = []; 10 | var idToTranslator = {}; 11 | var filenameToTranslator = {}; 12 | const rootPath = path.join(__dirname, '../..'); 13 | const infoRe = /^\s*{[\S\s]*?}\s*?[\r\n]/; 14 | 15 | async function loadTranslators() { 16 | if (!translators.length) { 17 | const files = await fs.readdir(rootPath); 18 | for (const file of files) { 19 | const fullPath = path.join(rootPath, file); 20 | if (!fullPath.endsWith('.js') || !(await fs.stat(fullPath)).isFile()) continue; 21 | let content = await fs.readFile(fullPath); 22 | let translatorInfo = JSON.parse(infoRe.exec(content)[0]); 23 | let translator = { metadata: translatorInfo, content }; 24 | translators.push(translator); 25 | filenameToTranslator[file] = translator; 26 | idToTranslator[translatorInfo.translatorID] = translator; 27 | } 28 | } 29 | } 30 | 31 | async function serveMetadata(req, res) { 32 | if (!translators.length) await loadTranslators(); 33 | res.writeHead(200); 34 | res.end(JSON.stringify(translators.map(t => t.metadata))); 35 | } 36 | 37 | async function serveCode(req, res) { 38 | const id = decodeURI(req.url.split('/')[2].split('?')[0]); 39 | try { 40 | res.writeHead(200); 41 | res.end(idToTranslator[id].content); 42 | } 43 | catch (e) { 44 | res.writeHead(404); 45 | res.end(); 46 | } 47 | } 48 | 49 | async function requestListener(req, res) { 50 | if (req.url.startsWith('/metadata')) { 51 | return serveMetadata(req, res); 52 | } else if (req.url.startsWith('/code')) { 53 | return serveCode(req, res); 54 | } 55 | res.writeHead(404); 56 | res.end(); 57 | } 58 | 59 | module.exports = { 60 | serve: async function() { 61 | await loadTranslators(); 62 | server = http.createServer(requestListener); 63 | server.listen(port, host, () => { 64 | console.log(`Translator server is running on http://${host}:${port}`); 65 | }); 66 | }, 67 | stopServing: function() { 68 | server.close(); 69 | }, 70 | filenameToTranslator, 71 | translators 72 | }; 73 | -------------------------------------------------------------------------------- /.ci/pull-request-check/xvfb-run-chrome: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | /usr/bin/xvfb-run /usr/bin/google-chrome $@ -------------------------------------------------------------------------------- /.ci/updateTypes.mjs: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { readFile, writeFile } from 'fs/promises'; 4 | 5 | const INDEX_D_TS_URL = new URL('../index.d.ts', import.meta.url); 6 | const SCHEMA_JSON_URL = new URL('../../zotero-client/resource/schema/global/schema.json', import.meta.url); 7 | 8 | const BEGIN_MARKER = '\t/* *** BEGIN GENERATED TYPES *** */'; 9 | const END_MARKER = '\t/* *** END GENERATED TYPES *** */'; 10 | 11 | async function updateIndexDTS() { 12 | let indexDTS = await readFile(INDEX_D_TS_URL, { encoding: 'utf8' }); 13 | let schema = JSON.parse(await readFile(SCHEMA_JSON_URL)); 14 | 15 | let typeItemTypes = '\ttype ItemTypes = {'; 16 | let itemTypeTypes = ''; 17 | let creatorTypes = new Set(); 18 | 19 | for (let typeSchema of schema.itemTypes) { 20 | let itemType = typeSchema.itemType; 21 | if (['annotation', 'attachment', 'note'].includes(itemType)) { 22 | continue; 23 | } 24 | 25 | let itemTypeUppercase = itemType[0].toUpperCase() + itemType.substring(1) + 'Item'; 26 | if (itemTypeUppercase == 'TvBroadcastItem') { 27 | itemTypeUppercase = 'TVBroadcastItem'; 28 | } 29 | 30 | typeItemTypes += `\n\t\t"${itemType}": ${itemTypeUppercase},`; 31 | itemTypeTypes += `\n\n\ttype ${itemTypeUppercase} = {`; 32 | itemTypeTypes += `\n\t\titemType: "${itemType}";`; 33 | for (let { field } of typeSchema.fields) { 34 | itemTypeTypes += `\n\t\t${field}?: string;` 35 | } 36 | 37 | let creatorTypesJoined = typeSchema.creatorTypes.map(typeSchema => '"' + typeSchema.creatorType + '"').join(' | '); 38 | itemTypeTypes += `\n\n\t\tcreators: Creator<${creatorTypesJoined}>[];`; 39 | itemTypeTypes += '\n\t\tattachments: Attachment[];'; 40 | itemTypeTypes += '\n\t\ttags: Tag[];'; 41 | itemTypeTypes += '\n\t\tnotes: Note[];'; 42 | itemTypeTypes += '\n\t\tseeAlso: string[];'; 43 | itemTypeTypes += '\n\t\tcomplete(): void;'; 44 | itemTypeTypes += '\n\n\t\t[key: string]: string;'; 45 | itemTypeTypes += '\n\t};'; 46 | 47 | for (let { creatorType } of typeSchema.creatorTypes) { 48 | creatorTypes.add(creatorType); 49 | } 50 | } 51 | typeItemTypes += '\n\t};' 52 | 53 | let typeCreatorType = '\n\ttype CreatorType ='; 54 | for (let creatorType of Array.from(creatorTypes).sort()) { 55 | typeCreatorType += `\n\t\t| "${creatorType}"`; 56 | } 57 | typeCreatorType += ';'; 58 | 59 | let beginIdx = indexDTS.indexOf(BEGIN_MARKER); 60 | let endIdx = indexDTS.indexOf(END_MARKER); 61 | if (beginIdx == -1 || endIdx == -1) { 62 | throw new Error('Could not find generated types section in index.d.ts'); 63 | } 64 | 65 | indexDTS = indexDTS.substring(0, beginIdx) + BEGIN_MARKER + '\n' 66 | + typeItemTypes 67 | + itemTypeTypes 68 | + '\n' + typeCreatorType 69 | + '\n' 70 | + indexDTS.substring(endIdx); 71 | 72 | await writeFile(INDEX_D_TS_URL, indexDTS); 73 | } 74 | 75 | updateIndexDTS(); 76 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | charset = utf-8 6 | trim_trailing_whitespace = true 7 | end_of_line = lf 8 | insert_final_newline = true 9 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "es2021": true 5 | }, 6 | "extends": [ 7 | "@zotero" 8 | ], 9 | "parserOptions": { 10 | "ecmaVersion": "2023" 11 | }, 12 | "globals": { 13 | "Zotero": "readonly", 14 | "Z": "readonly", 15 | "ZU": "readonly", 16 | "attr": "readonly", 17 | "innerText": "readonly", 18 | "text": "readonly", 19 | "request": "readonly", 20 | "requestText": "readonly", 21 | "requestJSON": "readonly", 22 | "requestDocument": "readonly" 23 | }, 24 | "rules": { 25 | "no-unused-vars": [ 26 | "error", 27 | { 28 | "argsIgnorePattern": "^_", 29 | "varsIgnorePattern": "^testCases$|^detectWeb$|^doWeb$|^detectImport$|^doImport$|^doExport$|^detectSearch$|^doSearch$|^exports$" 30 | } 31 | ], 32 | 33 | "no-redeclare": ["error", {"builtinGlobals": true}], 34 | "linebreak-style": ["error", "unix"], 35 | 36 | "lines-around-comment": [ 37 | "error", 38 | { 39 | "ignorePattern": "END TEST CASES" 40 | } 41 | ], 42 | 43 | "no-restricted-globals": ["error", { 44 | "name": "document", 45 | "message": "Use doc instead." 46 | }], 47 | 48 | "zotero-translator/not-executable": "error", 49 | "zotero-translator/header-valid-json": "error", 50 | "zotero-translator/header-translator-id": "error", 51 | "zotero-translator/header-last-updated": "warn", 52 | "zotero-translator/header-translator-type": "warn", 53 | 54 | "zotero-translator/no-for-each": "warn", 55 | "zotero-translator/prefer-index-of": "warn", 56 | "zotero-translator/robust-query-selector": "warn", 57 | 58 | "zotero-translator/test-cases": "error", 59 | "zotero-translator/translator-framework": "warn", 60 | 61 | "zotero-translator/license": [ "warn", { 62 | "mustMatch": "GNU Affero General Public License", 63 | "templateFile": ".ci/AGPL" 64 | }] 65 | }, 66 | "plugins": [ 67 | "zotero-translator" 68 | ], 69 | "processor": "zotero-translator/translator" 70 | } 71 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/T1_bug.yaml: -------------------------------------------------------------------------------- 1 | name: 报告转换器错误❌ 2 | description: 使用转换器时遇到问题?请告诉我们! 3 | title: "[Bug]: " 4 | labels: ["bug"] 5 | body: 6 | - type: checkboxes 7 | id: what-happened 8 | attributes: 9 | label: 你遇到了什么问题? 10 | description: 请选择你遇到的问题类型。 11 | options: 12 | - label: 无法识别条目(Zotero Connector显示蓝色空白图标) 13 | - label: 无法保存条目(保存时显示“使用xxx保存时发生错误”) 14 | - label: 无法下载附件 15 | - label: 缺少字段、字段错误 16 | - label: 其他问题 17 | validations: 18 | required: true 19 | - type: input 20 | id: url 21 | attributes: 22 | label: 链接 23 | description: 网站的网址,或文献交换格式的文档地址,请填写具体的链接,不要填网站主页。 24 | placeholder: https://link.cnki.net/doi/10.16278/j.cnki.cn13-1260/d.2021.02.003 25 | validations: 26 | required: true 27 | - type: textarea 28 | id: description 29 | attributes: 30 | label: 情况描述 31 | description: 请提供详细的描述,包括你遇到的问题、你期望的结果、你尝试过的方法等。 32 | validations: 33 | required: true 34 | - type: checkboxes 35 | id: browser 36 | attributes: 37 | label: 浏览器 38 | description: 请选择你使用的浏览器。 39 | options: 40 | - label: Chrome(谷歌浏览器) 41 | - label: Firefox(火狐浏览器) 42 | - label: Safari 43 | - label: Edge 44 | - label: 其他浏览器 45 | validations: 46 | required: true 47 | - type: checkboxes 48 | id: check-list 49 | attributes: 50 | label: 自查清单 51 | description: 请确保你已经按照以下步骤进行了自查。 52 | options: 53 | - label: 我使用最新正式版的Zotero 54 | required: true 55 | - label: 我使用最新正式版的浏览器 56 | required: true 57 | - label: 我已经按照[教程](https://zotero-chinese.com/user-guide/faqs/update-translators.html)将转换器更新到最新版 58 | required: true 59 | - label: 我尝试过重启浏览器、Zotero或电脑 60 | required: true 61 | - label: 我尝试过清除浏览器缓存(Cookies、Local Storage等) 62 | required: true 63 | - label: 我尝试过使用校园网直接访问网站,或者为 VPN 代理过的网站 [配置 Proxy](https://zotero-chinese.com/user-guide/faqs/off-campus-access) 64 | required: true 65 | - type: textarea 66 | id: attachments 67 | attributes: 68 | label: 附件 69 | description: 请提供截图或Zotero Connector的报错日志。 70 | - type: checkboxes 71 | id: report-check 72 | attributes: 73 | label: issue有效性检查 74 | description: 请确认你提交的issue满足以下要求。 75 | options: 76 | - label: 我填写了issue标题。 77 | required: true 78 | - label: 我反馈的转换器是本仓库维护的,可以在[转换器主页](https://zotero-chinese.com/translators/)中找到。 79 | required: true 80 | - label: 我已经搜索过现有issue,确认没有重复。 81 | required: true 82 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/T2_enhancement.yaml: -------------------------------------------------------------------------------- 1 | name: 请求支持新功能💪 2 | description: 为现有转换器添加新功能。 3 | title: "[Enhancement]: " 4 | labels: ["enhancement"] 5 | assignees: 6 | - jiaojiaodubai 7 | body: 8 | - type: input 9 | id: url 10 | attributes: 11 | label: 链接 12 | description: 网站的网址,或文献交换格式的文档地址。 13 | validations: 14 | required: true 15 | - type: checkboxes 16 | id: features 17 | attributes: 18 | label: 你想添加的功能 19 | description: 请选择你希望添加的功能。 20 | options: 21 | - label: 支持抓取/导出附件 22 | - label: 支持抓取网页快照 23 | - label: 支持抓取引用数 24 | validations: 25 | required: true 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/T3_new_translator.yaml: -------------------------------------------------------------------------------- 1 | name: 申请新转换器🙋‍♂️ 2 | description: 申请新的转换器,使Zotero能够从新的网站或文献交换格式导入,或者导出为新的文献交换格式。 3 | title: "[New]: " 4 | labels: ["new translator"] 5 | body: 6 | - type: markdown 7 | attributes: 8 | value: | 9 | - 这个模板仅用于为**尚未支持**的网站或文献交换格式申请转换器。 10 | - 本仓库仅维护中文(含简体中文、繁体中文、少数民族文字)语言地区的转换器,其他语言的转换器请到[官方仓库](https://github.com/zotero/translators)申请。 11 | - **请不要**用这个模板反馈bug,bug应使用[bug模板](https://github.com/l0o0/translators_CN/issues/new?template=T1_bug.yaml)。 12 | - **更不要**误认为此模板可以“更新转换器”,当前茉莉花已经支持自动更新转换器,更新转换器的教程见[此处](https://zotero-chinese.com/user-guide/faqs/update-translators)。 13 | - type: checkboxes 14 | id: type 15 | attributes: 16 | label: 转换器类型 17 | description: 请选择转换器的类型。 18 | options: 19 | - label: 网页转换器(用于从网页抓取条目) 20 | - label: 导入转换器(用于将条目导出为某种文献交换格式) 21 | - label: 导出转换器(用于从某种文献交换格式导入条目) 22 | validations: 23 | required: true 24 | - type: input 25 | id: url 26 | attributes: 27 | label: 链接 28 | description: 网站的网址,或文献交换格式的文档地址。 29 | placeholder: 例如:https://wwww.cnki.net/ 30 | validations: 31 | required: true 32 | - type: checkboxes 33 | id: item-type 34 | attributes: 35 | label: 条目类型 36 | description: 可抓取/导入/导出的条目类型 37 | options: 38 | - label: 期刊论文 39 | - label: 报纸文章 40 | - label: 会议论文 41 | - label: 学位论文 42 | - label: 图书 43 | - label: 书籍章节 44 | - label: 标准 45 | - label: 专利 46 | - label: 报告 47 | - label: 法律法规 48 | - label: 案件 49 | - label: 论坛帖子 50 | - label: 博客文章 51 | - label: 视频 52 | - label: 音频 53 | - label: 图像 54 | - label: 其他条目类型 55 | validations: 56 | required: true 57 | - type: checkboxes 58 | id: permissions 59 | attributes: 60 | label: 权限 61 | description: 请描述访问相关资源(如网站)时需要的特殊权限。 62 | options: 63 | - label: 注册帐号 64 | - label: 机构帐号 65 | - label: 付费帐号 66 | - label: IP认证 67 | - label: 其他权限 68 | - type: markdown 69 | id: note 70 | attributes: 71 | value: | 72 | 注:如果网站需要特殊权限(如机构认证、会员帐号)才能打开详情页面,请联系邮箱 帮助开发者访问网站,以便尽早完成开发。 73 | - type: checkboxes 74 | id: report-check 75 | attributes: 76 | label: 提交前检查 77 | description: 请确认你提交的issue满足以下要求。 78 | options: 79 | - label: 我填写了issue标题。 80 | required: true 81 | - label: 我确实是想申请新的转换器,而不是更新现有转换器。 82 | required: true 83 | - label: 我想适配的网站或文献交换格式是中文语言地区的。 84 | required: true 85 | - label: 我到[转换器主页](https://zotero-chinese.com/translators/)检查过,确认现有转换器尚未支持我要申请的网站或文献交换格式。 86 | required: true 87 | - label: 我已经搜索过现有issue,确认没有重复。 88 | required: true 89 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: [push, pull_request] 3 | jobs: 4 | build: 5 | name: Lint, Check, Test 6 | runs-on: ubuntu-latest 7 | timeout-minutes: 10 8 | steps: 9 | - uses: actions/checkout@v2 10 | with: 11 | submodules: true 12 | 13 | # We're in a shallow single-branch clone, so get the origin/master HEAD if not already on master 14 | # and get more history on the current branch so we can find the branch point 15 | - run: git fetch origin master:master --depth=1 16 | if: github.ref != 'refs/heads/master' 17 | - run: git fetch --update-shallow --depth=100 origin $(git rev-list HEAD) 18 | 19 | - name: Install Node 20 | uses: actions/setup-node@v2 21 | with: 22 | node-version: 17 23 | 24 | # Local via act 25 | - name: Install packages for act 26 | if: env.ACT == 'true' 27 | run: apt update && apt install -y xvfb git rsync 28 | 29 | - name: Get current connector hash 30 | id: get-connector-hash 31 | run: | 32 | echo "::set-output name=hash::$(git ls-remote https://github.com/zotero/zotero-connectors.git refs/heads/master | awk -F '\\s+' '{print $1}')" 33 | shell: bash 34 | 35 | - name: Cache connector code 36 | id: connector-cache 37 | uses: actions/cache@v4 38 | with: 39 | path: .ci/pull-request-check/connectors 40 | key: connectors-${{ hashFiles('.ci/pull-request-check/check-pull-request.sh') }}-${{ steps.get-connector-hash.outputs.hash }} 41 | 42 | - name: Install node packages 43 | run: npm ci 44 | 45 | #- name: Debugging with tmate 46 | # uses: mxschmitt/action-tmate@v3.1 47 | 48 | - name: Test pull request 49 | if: github.event_name == 'pull_request' 50 | env: 51 | BROWSER_EXECUTABLE: /home/runner/work/translators_CN/translators_CN/.ci/pull-request-check/xvfb-run-chrome 52 | run: ./check-pull-request.sh 53 | working-directory: .ci/pull-request-check 54 | 55 | - name: Check deleted.txt 56 | run: ./checkDeletedTxt.sh 57 | working-directory: .ci 58 | if: ${{ success() || failure() }} 59 | 60 | - name: Lint 61 | run: ./lint.sh 62 | working-directory: .ci 63 | if: ${{ success() || failure() }} 64 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Auto update translator metadata 2 | on: 3 | push: 4 | branches: 5 | - master 6 | - test 7 | 8 | paths: 9 | - '*.js' 10 | workflow_dispatch: 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | steps: 15 | # copy git 仓库到虚拟机上 16 | - name: 'Checkout codes' 17 | uses: actions/checkout@v3 18 | with: 19 | fetch-depth: 0 20 | 21 | # fix fatal: ambiguous argument 'HEAD~1': unknown revision or path not in the working tree. 22 | - run: git checkout master 23 | 24 | - name: Install Node 25 | uses: actions/setup-node@v2 26 | with: 27 | node-version: 17 28 | 29 | # Local via act 30 | - name: Install packages for act 31 | if: env.ACT == 'true' 32 | run: apt update && apt install -y xvfb git rsync 33 | 34 | - name: Install node packages 35 | run: npm ci 36 | 37 | - name: Update metadata 38 | run: | 39 | node data/updateJSON.js 40 | 41 | - name: Amend to original commit 42 | run: | 43 | git config --global user.name "GitHub Actions Bot" 44 | git config --global user.email "actions@github.com" 45 | git add data/*.json 46 | git commit --amend --no-edit 47 | git push origin HEAD --force 48 | -------------------------------------------------------------------------------- /.github/workflows/syncCI.yml: -------------------------------------------------------------------------------- 1 | name: Sync CI 2 | 3 | on: 4 | workflow_dispatch: 5 | schedule: 6 | - cron: '0 0 1 * *' # 每个月的第一天执行 7 | 8 | jobs: 9 | sync: 10 | runs-on: ubuntu-latest 11 | permissions: 12 | contents: write 13 | 14 | steps: 15 | - name: Checkout this repository 16 | uses: actions/checkout@v3 17 | with: 18 | path: this 19 | token: ${{ secrets.WORKFLOW_TOKEN }} 20 | 21 | - name: Checkout official repository 22 | uses: actions/checkout@v3 23 | with: 24 | repository: zotero/translators 25 | path: official 26 | token: ${{ secrets.WORKFLOW_TOKEN }} 27 | 28 | - name: Copy CI related folders and files from official to this repository 29 | run: | 30 | cd this 31 | rm -rf .ci 32 | cp -rf ../official/.ci ./ 33 | sed -i 's/\btranslators\/translators\b/translators_CN\/translators_CN/g' ../official/.github/workflows/ci.yml 34 | cp -f ../official/.github/workflows/ci.yml ./.github/workflows/ci.yml 35 | cp -f ../official/.eslintrc ./.eslintrc 36 | cp -f ../official/index.d.ts ./index.d.ts 37 | cp -f ../official/jsconfig.json ./jsconfig.json 38 | cp -f ../official/package-lock.json ./package-lock.json 39 | cp -f ../official/package.json ./package.json 40 | 41 | - name: Commit and push changes to this repository 42 | uses: EndBug/add-and-commit@v9 43 | with: 44 | cwd: './this' 45 | author_name: GitHub Actions Bot 46 | author_email: actions@github.com 47 | message: 'Sync CI with official repository' 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "javascript.format.enable": false, 3 | "javascript.validate.enable": false, 4 | "eslint.enable": true, 5 | "files.eol": "\n" 6 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | // See https://go.microsoft.com/fwlink/?LinkId=733558 3 | // for the documentation about the tasks.json format 4 | "version": "2.0.0", 5 | "tasks": [ 6 | { 7 | "label": "lint current JS file", 8 | "type": "shell", 9 | "command": "npx", 10 | "args": [ 11 | "eslint", 12 | "--fix", 13 | // see https://github.com/microsoft/vscode/issues/31722 14 | { 15 | "value": "${fileBasename}", 16 | "quoting": "escape" 17 | } 18 | ], 19 | "problemMatcher": "$eslint-stylish", 20 | "group": { 21 | "kind": "build", 22 | "isDefault": true 23 | } 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /Baidu Baike.js: -------------------------------------------------------------------------------- 1 | { 2 | "translatorID": "867474da-38d5-48eb-90cf-64e90aeb04d3", 3 | "label": "Baidu Baike", 4 | "creator": "pixiandouban", 5 | "target": "^https?://baike\\.baidu\\.(com|hk)", 6 | "minVersion": "2.1.9", 7 | "maxVersion": "", 8 | "priority": 100, 9 | "inRepository": true, 10 | "translatorType": 4, 11 | "browserSupport": "gcsibv", 12 | "lastUpdated": "2024-07-09 14:32:38" 13 | } 14 | 15 | /* 16 | ***** BEGIN LICENSE BLOCK ***** 17 | 18 | Copyright © 2021 pixiandouban 19 | 20 | This file is part of Zotero. 21 | 22 | Zotero is free software: you can redistribute it and/or modify 23 | it under the terms of the GNU Affero General Public License as published by 24 | the Free Software Foundation, either version 3 of the License, or 25 | (at your option) any later version. 26 | 27 | Zotero is distributed in the hope that it will be useful, 28 | but WITHOUT ANY WARRANTY; without even the implied warranty of 29 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 30 | GNU Affero General Public License for more details. 31 | 32 | You should have received a copy of the GNU Affero General Public License 33 | along with Zotero. If not, see . 34 | 35 | ***** END LICENSE BLOCK ***** 36 | */ 37 | 38 | function detectWeb(doc, url) { 39 | if (url.includes('/item/')) { 40 | return 'encyclopediaArticle'; 41 | } 42 | else if (getSearchResults(doc, true)) { 43 | return 'multiple'; 44 | } 45 | return false; 46 | } 47 | 48 | function getSearchResults(doc, checkOnly) { 49 | const items = {}; 50 | let found = false; 51 | const rows = doc.querySelectorAll('a[class^="title_"][href*="/item/"]'); 52 | for (const row of rows) { 53 | const href = row.href; 54 | const title = ZU.trimInternal(row.textContent); 55 | if (!href || !title) continue; 56 | if (checkOnly) return true; 57 | found = true; 58 | items[href] = title; 59 | } 60 | return found ? items : false; 61 | } 62 | 63 | async function doWeb(doc, url) { 64 | if (detectWeb(doc, url) == 'multiple') { 65 | let items = await Zotero.selectItems(getSearchResults(doc, false)); 66 | if (!items) return; 67 | for (const url of Object.keys(items)) { 68 | scrape(await requestDocument(url)); 69 | } 70 | } 71 | else { 72 | scrape(doc, url); 73 | } 74 | } 75 | 76 | function scrape(doc, _url) { 77 | const newItem = new Zotero.Item('encyclopediaArticle'); 78 | newItem.title = text(doc, 'h1[class*="title"]'); 79 | newItem.abstractNote = attr(doc, 'meta[name="description"]', 'content'); 80 | newItem.encyclopediaTitle = '百度百科'; 81 | newItem.date = attr(doc, 'meta[itemprop="dateUpdate"]', 'content'); 82 | newItem.url = attr(doc, 'link[rel="canonical"]', 'href'); 83 | newItem.libraryCatalog = '百度百科'; 84 | newItem.language = 'zh-CN'; 85 | newItem.attachments.push({ 86 | title: 'Snapshot', 87 | document: doc 88 | }); 89 | newItem.complete(); 90 | } 91 | 92 | /** BEGIN TEST CASES **/ 93 | var testCases = [ 94 | { 95 | "type": "web", 96 | "url": "https://baike.baidu.com/item/%E6%9D%8E%E9%B8%BF%E7%AB%A0/28575", 97 | "items": [ 98 | { 99 | "itemType": "encyclopediaArticle", 100 | "title": "李鸿章", 101 | "creators": [], 102 | "date": "2023-11-17 09:52:47", 103 | "abstractNote": "李鸿章(1823年2月15日-1901年11月7日),本名章铜,字渐甫、子黻,号少荃(一作少泉),晚年自号仪叟,别号省心,安徽省庐州府合肥县磨店乡(今属合肥市)人,中国清朝晚期政治家、外交家、军事将领。世人多称“李中堂”,又称“李二先生”“李傅相”“李文忠”。李鸿章为道光二十七年(1847年)进士,早年随业师曾国藩镇压太平天国运动与捻军起义,并受命组建淮军,因战功擢升至直隶总督,兼北洋通商大臣,累加至文华殿大学士,封一等肃毅伯。期间参与清廷在外交、军事、经济等方面的重大事务,先后创办江南制造局、轮船招商局、上海机器织布局和上海广方言馆等洋务机构,又组建了北洋水师。甲午战争中,因诸种失误,使北洋水师覆没,战后作为特使与日本签订《马关条约》。光绪二十五年(1899年),被启用为两广总督,翌年八国联军侵华战争爆发后,参与“东南互保”,并奉命北上谈判。光绪二十七年(1901年),李鸿章与庆亲王奕劻", 104 | "encyclopediaTitle": "百度百科", 105 | "language": "zh-CN", 106 | "libraryCatalog": "百度百科", 107 | "url": "https://baike.baidu.com/item/%E6%9D%8E%E9%B8%BF%E7%AB%A0/28575", 108 | "attachments": [ 109 | { 110 | "title": "Snapshot", 111 | "mimeType": "text/html" 112 | } 113 | ], 114 | "tags": [], 115 | "notes": [], 116 | "seeAlso": [] 117 | } 118 | ] 119 | }, 120 | { 121 | "type": "web", 122 | "url": "https://baike.baidu.com/item/%E5%A4%A9%E6%B0%94/24449?fromModule=search-result_lemma", 123 | "items": [ 124 | { 125 | "itemType": "encyclopediaArticle", 126 | "title": "天气", 127 | "creators": [], 128 | "date": "2024-01-05 08:30:32", 129 | "abstractNote": "天气(weather)是指某一个地区距离地表较近的大气层在短时间内的具体状态。而天气现象则是指发生在大气中的各种自然现象,即某瞬时内大气中各种气象要素(如气温、气压、湿度、风、云、雾、雨、闪、雪、霜、雷、雹、霾等)空间分布的综合表现。天气过程就是一定地区的天气现象随时间的变化过程。各种天气系统都具有一定的空间尺度和时间尺度,而且各种尺度系统间相互交织、相互作用。许多天气系统的组合,构成大范围的天气形势,构成半球甚至全球的大气环流。天气系统总是处在不断新生、发展和消亡过程中,在不同发展阶段有着其相对应的天气现象分布。", 130 | "encyclopediaTitle": "百度百科", 131 | "language": "zh-CN", 132 | "libraryCatalog": "百度百科", 133 | "url": "https://baike.baidu.com/item/%E5%A4%A9%E6%B0%94/24449", 134 | "attachments": [ 135 | { 136 | "title": "Snapshot", 137 | "mimeType": "text/html" 138 | } 139 | ], 140 | "tags": [], 141 | "notes": [], 142 | "seeAlso": [] 143 | } 144 | ] 145 | }, 146 | { 147 | "type": "web", 148 | "url": "https://baike.baidu.com/search?word=%E5%A4%A9%E6%B0%94&pn=0&rn=0&enc=utf8", 149 | "items": "multiple" 150 | } 151 | ] 152 | /** END TEST CASES **/ 153 | -------------------------------------------------------------------------------- /CHINESE JOURNAL OF LAW.js: -------------------------------------------------------------------------------- 1 | { 2 | "translatorID": "d37a0069-cbc7-4b91-8c04-3be2c3bc3e6e", 3 | "label": "CHINESE JOURNAL OF LAW", 4 | "creator": "jiaojiaodubai", 5 | "target": "^https://faxueyanjiu\\.ajcass\\.com", 6 | "minVersion": "5.0", 7 | "maxVersion": "", 8 | "priority": 100, 9 | "inRepository": true, 10 | "translatorType": 4, 11 | "browserSupport": "gcsibv", 12 | "lastUpdated": "2024-07-09 15:13:15" 13 | } 14 | 15 | /* 16 | ***** BEGIN LICENSE BLOCK ***** 17 | 18 | Copyright © 2024 jiaojiaodubai 19 | 20 | This file is part of Zotero. 21 | 22 | Zotero is free software: you can redistribute it and/or modify 23 | it under the terms of the GNU Affero General Public License as published by 24 | the Free Software Foundation, either version 3 of the License, or 25 | (at your option) any later version. 26 | 27 | Zotero is distributed in the hope that it will be useful, 28 | but WITHOUT ANY WARRANTY; without even the implied warranty of 29 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 30 | GNU Affero General Public License for more details. 31 | 32 | You should have received a copy of the GNU Affero General Public License 33 | along with Zotero. If not, see . 34 | 35 | ***** END LICENSE BLOCK ***** 36 | */ 37 | 38 | 39 | function detectWeb(doc, _url) { 40 | if (getSearchResults(doc, true)) { 41 | return 'multiple'; 42 | } 43 | return false; 44 | } 45 | 46 | function getSearchResults(doc, checkOnly) { 47 | return doc.querySelector('.gdkuang') 48 | ? getListItems(doc, checkOnly) 49 | : getTableItems(doc, checkOnly); 50 | } 51 | 52 | function getTableItems(doc, checkOnly) { 53 | const items = {}; 54 | let found = false; 55 | const rows = Array.from(doc.querySelectorAll('#tbody24 > tr')).filter(elm => elm.querySelector('input')); 56 | for (const row of rows) { 57 | const href = attr(row, 'a:only-child', 'href'); 58 | const title = ZU.trimInternal(row.textContent); 59 | let pubInfo = ''; 60 | try { 61 | pubInfo = row.nextElementSibling.nextElementSibling.innerText; 62 | } 63 | catch (error) { 64 | Z.debug(error); 65 | } 66 | if (!href || !title) continue; 67 | if (checkOnly) return true; 68 | found = true; 69 | items[JSON.stringify({ 70 | url: href, 71 | pubInfo: pubInfo 72 | })] = title; 73 | } 74 | return found ? items : false; 75 | } 76 | 77 | function getListItems(doc, checkOnly) { 78 | const items = {}; 79 | let found = false; 80 | const rows = doc.querySelectorAll('.gdkuang > ul'); 81 | for (const row of rows) { 82 | const href = attr(row, 'li:first-of-type > a', 'href'); 83 | const title = text(row, 'li:first-of-type > a'); 84 | const pubInfo = text(row, 'li:last-child tr:only-child > td:first-child'); 85 | if (!href || !title) continue; 86 | if (checkOnly) return true; 87 | found = true; 88 | items[JSON.stringify({ 89 | url: href, 90 | pubInfo: pubInfo 91 | })] = title; 92 | } 93 | return found ? items : false; 94 | } 95 | 96 | async function doWeb(doc, url) { 97 | if (detectWeb(doc, url) == 'multiple') { 98 | let items = await Zotero.selectItems(getSearchResults(doc, false)); 99 | if (!items) return; 100 | for (const key of Object.keys(items)) { 101 | await scrape(JSON.parse(key)); 102 | } 103 | } 104 | } 105 | 106 | async function scrape(keyObj) { 107 | const itemDoc = await requestDocument(keyObj.url); 108 | const pubInfo = keyObj.pubInfo.trim(); 109 | Z.debug(pubInfo); 110 | const newItem = new Z.Item('journalArticle'); 111 | newItem.title = text(itemDoc, '#FileTitle'); 112 | newItem['original-title'] = ZU.capitalizeTitle(text(itemDoc, '#EnTitle')); 113 | newItem.abstractNote = text(itemDoc, '#Abstract'); 114 | newItem.publicationTitle = '法学研究'; 115 | newItem['original-container-title'] = 'Chinese Journal of Law'; 116 | newItem.volume = tryMatch(pubInfo, /\.\(0*(\d+)\)/, 1) || tryMatch(pubInfo, /,0*(\d+)/, 1); 117 | newItem.issue = (tryMatch(pubInfo, /\)(.+?):/, 1) || tryMatch(pubInfo, /\((.+?)\)/, 1)).replace(/0*(\d+)/, '$1'); 118 | newItem.pages = tryMatch(pubInfo, /:([\d, ~+-]+)/, 1).replace(/\+/g, ', ').replace(/~/g, '-'); 119 | newItem.date = tryMatch(pubInfo, /^\d+/); 120 | newItem.DOI = text(itemDoc, '#DOI'); 121 | newItem.ISSN = '1002-896X'; 122 | text(itemDoc, '#Author tr:last-child td:first-child').split(/[;,;、]\s?/).forEach(string => newItem.creators.push({ 123 | firstName: '', 124 | lastName: string, 125 | creatorType: 'author', 126 | fieldMode: 1 127 | })); 128 | text(itemDoc, '#KeyWord').split(/[;,;、]\s?/).forEach(string => newItem.tags.push(string)); 129 | const pdfLink = itemDoc.querySelector('#URL > a'); 130 | if (pdfLink) { 131 | newItem.attachments.push({ 132 | url: pdfLink.href, 133 | title: 'Full Text PDF', 134 | mimeType: 'application/pdf' 135 | }); 136 | } 137 | newItem.complete(); 138 | } 139 | 140 | /** 141 | * Attempts to get the part of the pattern described from the character, 142 | * and returns an empty string if not match. 143 | * @param {String} string 144 | * @param {RegExp} pattern 145 | * @param {Number} index 146 | * @returns 147 | */ 148 | function tryMatch(string, pattern, index = 0) { 149 | if (!string) return ''; 150 | let match = string.match(pattern); 151 | return (match && match[index]) 152 | ? match[index] 153 | : ''; 154 | } 155 | 156 | /** BEGIN TEST CASES **/ 157 | var testCases = [ 158 | { 159 | "type": "web", 160 | "url": "https://faxueyanjiu.ajcass.com/Home/Index", 161 | "items": "multiple" 162 | }, 163 | { 164 | "type": "web", 165 | "url": "https://faxueyanjiu.ajcass.com/Magazine/GetIssueContentList2/?PageSize=12&Year=2023&Issue=2", 166 | "items": "multiple" 167 | } 168 | ] 169 | /** END TEST CASES **/ 170 | -------------------------------------------------------------------------------- /CSDN.js: -------------------------------------------------------------------------------- 1 | { 2 | "translatorID": "481b8759-56bd-465f-a783-d9d74b313749", 3 | "label": "CSDN", 4 | "creator": "jiaojiaodubai", 5 | "target": "^https://blog\\.csdn\\.net", 6 | "minVersion": "5.0", 7 | "maxVersion": "", 8 | "priority": 100, 9 | "inRepository": true, 10 | "translatorType": 4, 11 | "browserSupport": "gcsibv", 12 | "lastUpdated": "2024-11-03 02:35:05" 13 | } 14 | 15 | /* 16 | ***** BEGIN LICENSE BLOCK ***** 17 | 18 | Copyright © 2024 jiaojiaodubai 19 | 20 | This file is part of Zotero. 21 | 22 | Zotero is free software: you can redistribute it and/or modify 23 | it under the terms of the GNU Affero General Public License as published by 24 | the Free Software Foundation, either version 3 of the License, or 25 | (at your option) any later version. 26 | 27 | Zotero is distributed in the hope that it will be useful, 28 | but WITHOUT ANY WARRANTY; without even the implied warranty of 29 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 30 | GNU Affero General Public License for more details. 31 | 32 | You should have received a copy of the GNU Affero General Public License 33 | along with Zotero. If not, see . 34 | 35 | ***** END LICENSE BLOCK ***** 36 | */ 37 | 38 | 39 | function detectWeb(doc, url) { 40 | if (url.includes('/article/details/')) { 41 | return 'blogPost'; 42 | } 43 | else if (getSearchResults(doc, true)) { 44 | return 'multiple'; 45 | } 46 | return false; 47 | } 48 | 49 | function getSearchResults(doc, checkOnly) { 50 | const items = {}; 51 | let found = false; 52 | // '.Community-item > .content > a' for home page 53 | // '.item-hd > h3 > a' for search result 54 | const rows = doc.querySelectorAll('.Community-item > .content > a[href*="/article/details/"],.item-hd > h3 > a[href*="/article/details/"]'); 55 | for (const row of rows) { 56 | const href = row.href; 57 | const title = ZU.trimInternal(row.textContent); 58 | if (!href || !title) continue; 59 | if (checkOnly) return true; 60 | found = true; 61 | items[href] = title; 62 | } 63 | return found ? items : false; 64 | } 65 | 66 | async function doWeb(doc, url) { 67 | if (detectWeb(doc, url) == 'multiple') { 68 | const items = await Zotero.selectItems(getSearchResults(doc, false)); 69 | if (!items) return; 70 | for (const url of Object.keys(items)) { 71 | await scrape(await requestDocument(url)); 72 | } 73 | } 74 | else { 75 | await scrape(doc); 76 | } 77 | } 78 | 79 | async function scrape(doc) { 80 | const newItem = new Z.Item('blogPost'); 81 | newItem.title = text(doc, 'h1.title-article'); 82 | newItem.abstractNote = attr(doc, 'meta[name="description"]', 'content'); 83 | newItem.blogTitle = 'CSDN'; 84 | newItem.date = ZU.strToISO(attr(doc, '.blog-postTime', 'data-time')); 85 | newItem.url = attr(doc, 'link[rel="canonical"]', 'href'); 86 | newItem.language = 'zh-CN'; 87 | newItem.creators.push({ 88 | firstName: '', 89 | lastName: text(doc, '#uid'), 90 | creatorType: 'author', 91 | fieldMode: 1 92 | }); 93 | newItem.attachments.push({ 94 | title: 'Snapshot', 95 | document: doc 96 | }); 97 | doc.querySelectorAll('.tags-box > a').forEach(elm => newItem.tags.push(ZU.trimInternal(elm.textContent))); 98 | newItem.complete(); 99 | } 100 | 101 | /** BEGIN TEST CASES **/ 102 | var testCases = [ 103 | { 104 | "type": "web", 105 | "url": "https://blog.csdn.net/weixin_48093237/article/details/141571439", 106 | "items": [ 107 | { 108 | "itemType": "blogPost", 109 | "title": "zotero同步之infiniteCLOUD网盘 WebDAV", 110 | "creators": [ 111 | { 112 | "firstName": "", 113 | "lastName": "Curious!", 114 | "creatorType": "author", 115 | "fieldMode": 1 116 | } 117 | ], 118 | "date": "2024-08-26", 119 | "abstractNote": "文章浏览阅读425次,点赞5次,收藏5次。zotero打开 编辑->首选项->同步。_infinitecloud", 120 | "blogTitle": "CSDN", 121 | "language": "zh-CN", 122 | "url": "https://blog.csdn.net/weixin_48093237/article/details/141571439", 123 | "attachments": [ 124 | { 125 | "title": "Snapshot", 126 | "mimeType": "text/html" 127 | } 128 | ], 129 | "tags": [ 130 | { 131 | "tag": "ubuntu" 132 | }, 133 | { 134 | "tag": "zotero" 135 | } 136 | ], 137 | "notes": [], 138 | "seeAlso": [] 139 | } 140 | ] 141 | }, 142 | { 143 | "type": "web", 144 | "url": "https://blog.csdn.net/", 145 | "items": "multiple" 146 | }, 147 | { 148 | "type": "web", 149 | "url": "https://so.csdn.net/so/search?q=Zotero%E5%90%8C%E6%AD%A5", 150 | "items": "multiple" 151 | } 152 | ] 153 | /** END TEST CASES **/ 154 | -------------------------------------------------------------------------------- /Cubox.js: -------------------------------------------------------------------------------- 1 | { 2 | "translatorID": "992850d2-b68b-4a1f-8dd6-0f4fd323c6be", 3 | "label": "Cubox", 4 | "creator": "eatcosmos", 5 | "target": "https://cubox\\.pro/my/card", 6 | "minVersion": "5.0", 7 | "maxVersion": "", 8 | "priority": 100, 9 | "inRepository": true, 10 | "translatorType": 4, 11 | "browserSupport": "gcsibv", 12 | "lastUpdated": "2024-06-20 16:34:51" 13 | } 14 | 15 | 16 | /* 17 | ***** BEGIN LICENSE BLOCK ***** 18 | 19 | Copyright © 2023 eatcosmos 20 | 21 | This file is part of Zotero. 22 | 23 | Zotero is free software: you can redistribute it and/or modify 24 | it under the terms of the GNU Affero General Public License as published by 25 | the Free Software Foundation, either version 3 of the License, or 26 | (at your option) any later version. 27 | 28 | Zotero is distributed in the hope that it will be useful, 29 | but WITHOUT ANY WARRANTY; without even the implied warranty of 30 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 31 | GNU Affero General Public License for more details. 32 | 33 | You should have received a copy of the GNU Affero General Public License 34 | along with Zotero. If not, see . 35 | 36 | ***** END LICENSE BLOCK ***** 37 | */ 38 | 39 | function detectWeb(doc, url) { 40 | if (url.includes('card')) { 41 | return 'blogPost'; 42 | } 43 | else if (getSearchResults(doc, true)) { 44 | return 'multiple'; 45 | } 46 | else if (url.includes('ChatGPT')) { 47 | return 'blogPost'; 48 | } 49 | return false; 50 | } 51 | 52 | function getSearchResults(doc, checkOnly) { 53 | var items = {}; 54 | var found = false; 55 | var rows = doc.querySelectorAll('h2 > a.title[href*="/article/"]'); 56 | for (let row of rows) { 57 | let href = row.href; 58 | let title = ZU.trimInternal(row.textContent); 59 | if (!href || !title) continue; 60 | if (checkOnly) return true; 61 | found = true; 62 | items[href] = title; 63 | } 64 | return found ? items : false; 65 | } 66 | 67 | async function doWeb(doc, url) { 68 | if (detectWeb(doc, url) == 'multiple') { 69 | let items = await Zotero.selectItems(getSearchResults(doc, false)); 70 | if (!items) return; 71 | for (let url of Object.keys(items)) { 72 | await scrape(await requestDocument(url)); 73 | } 74 | } 75 | else { 76 | await scrape(doc, url); 77 | } 78 | } 79 | 80 | async function scrape(doc, url = doc.location.href) { 81 | // TODO: implement or add a scrape function template 82 | Z.debug("url: " + url); 83 | var title = ZU.xpath(doc, "//h1[@class='reader-title']"); // 返回的所有符合该条件的元素列表 84 | // Z.debug("title 1: " + title); 85 | title = title[0].innerText; // 因为从网页上看只有一个元素符合这个条件,就把第一个元素取出,它的文本就是标题内容 86 | Z.debug("title 2: " + title); 87 | // var publishDate = ZU.xpath(doc, "//head/meta[@name='publishdate']"); // 也是返回列表 88 | // var publishDate = publishDate[0].getAttribute('content'); // 取第一个元素,取得 content 属性值 89 | // Z.debug(publishDate); 90 | var author = ZU.xpath(doc, "//span[@class='reader-metadata-author']"); 91 | author = author[0].innerText; 92 | Z.debug("author: " + author); 93 | var originUrl = ZU.xpath(doc, "//a[@class='reader-footer-source']"); 94 | originUrl = originUrl[0].getAttribute('href'); 95 | Z.debug("origin_url: " + originUrl); 96 | 97 | var newItem = new Zotero.Item("blogPost"); // 新建一个新闻条目,后面把信息填入到对应字段 98 | newItem.title = title; 99 | // newItem.date = publishDate; 100 | // newItem.date = publishDate; 101 | newItem.blogTitle = title; 102 | newItem.url = url; 103 | newItem.creators.push({ lastName: author, creatorType: 'author' }); // 创建者信息,参考文本翻译器编写官方文档 104 | // newItem.notes.push({note:content}); // 这里是把内容放到条目下的笔记中 105 | // newItem.attachments.push({url:origin_url, title:title}); // 这里是把网页快照放到条目下的附件中 106 | newItem.extra = originUrl; // 这里是把原始网址放到条目下的附加信息中 107 | newItem.complete(); // 最后一定要有这一步,表示收集完成,可以传给 Zotero 108 | } 109 | 110 | /** BEGIN TEST CASES **/ 111 | var testCases = [ 112 | { 113 | "type": "web", 114 | "url": "http://observationalepidemiology.blogspot.com/2011/10/tweet-from-matt-yglesias.html", 115 | "items": [ 116 | { 117 | "itemType": "blogPost", 118 | "title": "A tweet from Matt Yglesias", 119 | "creators": [ 120 | { 121 | "firstName": "", 122 | "lastName": "Joseph", 123 | "creatorType": "author" 124 | } 125 | ], 126 | "date": "Monday, October 24, 2011", 127 | "accessDate": "CURRENT_TIMESTAMP", 128 | "blogTitle": "West Coast Stat Views (on Observational Epidemiology and more)", 129 | "libraryCatalog": "Blogger", 130 | "url": "http://observationalepidemiology.blogspot.com/2011/10/tweet-from-matt-yglesias.html", 131 | "attachments": [ 132 | { 133 | "title": "Blogspot Snapshot", 134 | "mimeType": "text/html" 135 | } 136 | ], 137 | "tags": [ 138 | "Mark", 139 | "Matthew Yglesias" 140 | ], 141 | "notes": [], 142 | "seeAlso": [] 143 | } 144 | ] 145 | }, 146 | { 147 | "type": "web", 148 | "url": "http://observationalepidemiology.blogspot.com/", 149 | "items": "multiple" 150 | }, 151 | { 152 | "type": "web", 153 | "url": "http://argentina-politica.blogspot.com/2012/03/perciben-una-caida-en-la-imagen-de-la.html", 154 | "items": [ 155 | { 156 | "itemType": "blogPost", 157 | "title": "Politica Argentina - Blog de Psicología Política de Federico González: Perciben una caída en la imagen de la Presidenta", 158 | "creators": [ 159 | { 160 | "firstName": "Federico", 161 | "lastName": "Gonzalez", 162 | "creatorType": "author" 163 | } 164 | ], 165 | "date": "domingo, 11 de marzo de 2012", 166 | "blogTitle": "Politica Argentina - Blog de Psicología Política de Federico González", 167 | "shortTitle": "Politica Argentina - Blog de Psicología Política de Federico González", 168 | "url": "http://argentina-politica.blogspot.com/2012/03/perciben-una-caida-en-la-imagen-de-la.html", 169 | "attachments": [ 170 | { 171 | "title": "Blogspot Snapshot", 172 | "mimeType": "text/html" 173 | } 174 | ], 175 | "tags": [ 176 | "Cristina Kirchner", 177 | "imagen" 178 | ], 179 | "notes": [], 180 | "seeAlso": [] 181 | } 182 | ] 183 | }, 184 | { 185 | "type": "web", 186 | "url": "http://utotherescue.blogspot.com/2013/11/the-heart-of-matter-humanities-do-more.html", 187 | "items": [ 188 | { 189 | "itemType": "blogPost", 190 | "title": "National Humanities Report Reinforces Stereotypes about the Humanities ~ Remaking the University", 191 | "creators": [ 192 | { 193 | "firstName": "Michael", 194 | "lastName": "Meranze", 195 | "creatorType": "author" 196 | } 197 | ], 198 | "date": "Monday, November 25, 2013", 199 | "blogTitle": "National Humanities Report Reinforces Stereotypes about the Humanities ~ Remaking the University", 200 | "url": "http://utotherescue.blogspot.com/2013/11/the-heart-of-matter-humanities-do-more.html", 201 | "attachments": [ 202 | { 203 | "title": "Blogspot Snapshot", 204 | "mimeType": "text/html" 205 | } 206 | ], 207 | "tags": [ 208 | "Cuts", 209 | "Development", 210 | "Humanities", 211 | "Liberal Arts", 212 | "guest post" 213 | ], 214 | "notes": [], 215 | "seeAlso": [] 216 | } 217 | ] 218 | }, 219 | { 220 | "type": "web", 221 | "url": "https://jamsubuntu.blogspot.com/2009/01/unmount-command-not-found.html", 222 | "items": [ 223 | { 224 | "itemType": "blogPost", 225 | "title": "Jam's Ubuntu Linux Blog: unmount: command not found", 226 | "creators": [], 227 | "date": "Wednesday, 7 January 2009", 228 | "blogTitle": "Jam's Ubuntu Linux Blog", 229 | "shortTitle": "Jam's Ubuntu Linux Blog", 230 | "url": "https://jamsubuntu.blogspot.com/2009/01/unmount-command-not-found.html", 231 | "attachments": [ 232 | { 233 | "title": "Blogspot Snapshot", 234 | "mimeType": "text/html" 235 | } 236 | ], 237 | "tags": [ 238 | "Command Line" 239 | ], 240 | "notes": [], 241 | "seeAlso": [] 242 | } 243 | ] 244 | } 245 | ] 246 | /** END TEST CASES **/ 247 | -------------------------------------------------------------------------------- /Dangdang.js: -------------------------------------------------------------------------------- 1 | { 2 | "translatorID": "ec98c7f1-1f76-43d1-a5fd-fc36428fba58", 3 | "label": "Dangdang", 4 | "creator": "018", 5 | "target": "^https?://(product|search)\\.dangdang\\.com/", 6 | "minVersion": "3.0", 7 | "maxVersion": "", 8 | "priority": 100, 9 | "inRepository": true, 10 | "translatorType": 4, 11 | "browserSupport": "gcsibv", 12 | "lastUpdated": "2023-12-15 19:39:16" 13 | } 14 | 15 | /* 16 | ***** BEGIN LICENSE BLOCK ***** 17 | 18 | Copyright © 2020 018 19 | 20 | This file is part of Zotero. 21 | 22 | Zotero is free software: you can redistribute it and/or modify 23 | it under the terms of the GNU Affero General Public License as published by 24 | the Free Software Foundation, either version 3 of the License, or 25 | (at your option) any later version. 26 | 27 | Zotero is distributed in the hope that it will be useful, 28 | but WITHOUT ANY WARRANTY; without even the implied warranty of 29 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 30 | GNU Affero General Public License for more details. 31 | 32 | You should have received a copy of the GNU Affero General Public License 33 | along with Zotero. If not, see . 34 | 35 | ***** END LICENSE BLOCK ***** 36 | */ 37 | 38 | function detectWeb(doc, url) { 39 | if (url.includes('product')) { 40 | return 'book'; 41 | } 42 | else if (url.includes('search') && getSearchResults(doc, true)) { 43 | return 'multiple'; 44 | } 45 | return ''; 46 | } 47 | 48 | function getSearchResults(doc, checkOnly) { 49 | var items = {}; 50 | var found = false; 51 | var rows = doc.querySelectorAll('#search_nature_rg ul li a'); 52 | for (let row of rows) { 53 | let href = row.href; 54 | let title = ZU.trimInternal(row.textContent); 55 | if (!href || !title) continue; 56 | if (checkOnly) return true; 57 | found = true; 58 | items[href] = title; 59 | } 60 | return found ? items : false; 61 | } 62 | 63 | async function doWeb(doc, url) { 64 | if (detectWeb(doc, url) == 'multiple') { 65 | let items = await Zotero.selectItems(getSearchResults(doc, false)); 66 | if (!items) return; 67 | for (let url of Object.keys(items)) { 68 | await scrape(await requestDocument(url)); 69 | } 70 | } 71 | else { 72 | await scrape(doc, url); 73 | } 74 | } 75 | 76 | async function scrape(doc, url = doc.location.href) { 77 | var newItem = new Zotero.Item('book'); 78 | // 标题 79 | newItem.title = attr(doc, '#product_info > div.name_info > h1', 'title'); 80 | // 作者 81 | newItem.creators = Array.from(doc.querySelectorAll('#author a')) 82 | .map(element => ZU.cleanAuthor(element.innerText, 'author')); 83 | newItem.creators.forEach((element) => { 84 | if (/[\u4e00-\u9fa5]/.test(element.lastName)) element.fieldMode = 1; 85 | }); 86 | 87 | var data = Array.from(doc.querySelectorAll('#detail_describe > ul li')) 88 | .map(element => element.innerText) 89 | .map(element => [tryMatch(element, /^(.+):/, 1), tryMatch(element, /:(.*)/, 1)]); 90 | data = { 91 | innerData: data, 92 | get: function (label) { 93 | let keyVal = this.innerData.find(element => element[0].startsWith(label)); 94 | return keyVal ? keyVal[1] : ''; 95 | } 96 | }; 97 | newItem.ISBN = data.get('国际标准书号ISBN'); 98 | newItem.series = data.get('丛书名'); 99 | // 出版社 100 | newItem.publisher = text(doc, 'a[dd_name="出版社"]'); 101 | // 出版年 102 | newItem.date = text(doc, '.messbox_info .t1:nth-child(3)').replace(/出版时间:/, '').replace(/\D$/, '') 103 | .replace(/\D+/g, '-'); 104 | // 简介 105 | newItem.abstractNote = text(doc, '.descrip > #content-show, #content > .descrip'); 106 | // 中图clc作为标签,需要安装油猴插件:https://greasyfork.org/zh-CN/scripts/408682 107 | newItem.archiveLocation = text(doc, '#clc'); 108 | newItem.archive = text(doc, '#subject'); 109 | newItem.url = url; 110 | newItem.complete(); 111 | } 112 | 113 | function tryMatch(string, pattern, index = 0) { 114 | let match = string.match(pattern); 115 | if (match && match[index]) { 116 | return match[index]; 117 | } 118 | return ''; 119 | } 120 | 121 | /** BEGIN TEST CASES **/ 122 | var testCases = [ 123 | { 124 | "type": "web", 125 | "url": "http://product.dangdang.com/29113890.html", 126 | "items": [ 127 | { 128 | "itemType": "book", 129 | "title": "个人理财(第11版)(金融学译丛)", 130 | "creators": [ 131 | { 132 | "firstName": "", 133 | "lastName": "E.托马斯", 134 | "creatorType": "author", 135 | "fieldMode": 1 136 | }, 137 | { 138 | "firstName": "", 139 | "lastName": "加曼", 140 | "creatorType": "author", 141 | "fieldMode": 1 142 | }, 143 | { 144 | "firstName": "", 145 | "lastName": "雷蒙德", 146 | "creatorType": "author", 147 | "fieldMode": 1 148 | }, 149 | { 150 | "firstName": "", 151 | "lastName": "E.福格", 152 | "creatorType": "author", 153 | "fieldMode": 1 154 | } 155 | ], 156 | "date": "2020-08", 157 | "ISBN": "9787300256535", 158 | "abstractNote": "《个人理财》时刻提醒读者记住一句至理名言:努力工作、学习、储蓄和投资,并且做好个人理财规划。本书修订至第11版,已成为世界范围内极有影响力的、知名的个人理财类图书。以“做什么、何时做以及怎样做”为主线,在个人理财方面为你出谋划策,指引你迈向成功。本书18章,分为五个部分,即理财规划、现金管理、收入和资产保护、投资以及退休和遗产规划。每个章节都力图阐述一些理财基本准则,以帮助读者在有生之年更好地进行个人理财。本修订版每章课后都安排了实践活动,新增了朱莉娅理财之旅连载故事,她成功的理财生涯贯穿整个具有挑战性的经济时期,在其故事的尾声,留给读者一个具有挑战性的问题: 根据她的想法,提出你的建议。《个人理财》(第11版)包括30多个新主题,新增100多个文本框、数十个新的名词以及海量的新扩展信息。新版本把个人理财中的各个知识碎片有机地连接起来,形成了一个具有全局性的知识体系,向读者展示了个人理财知识之间的内在联系。*后,预祝本书所有的读者都获得成功!", 159 | "libraryCatalog": "Dangdang", 160 | "publisher": "中国人民大学出版社", 161 | "series": "金融学译丛", 162 | "url": "http://product.dangdang.com/29113890.html", 163 | "attachments": [], 164 | "tags": [], 165 | "notes": [], 166 | "seeAlso": [] 167 | } 168 | ] 169 | }, 170 | { 171 | "type": "web", 172 | "url": "http://search.dangdang.com/?key=9787300256535&act=input", 173 | "items": "multiple" 174 | } 175 | ] 176 | /** END TEST CASES **/ 177 | -------------------------------------------------------------------------------- /Encyclopedia of China 3rd.js: -------------------------------------------------------------------------------- 1 | { 2 | "translatorID": "6a540908-0419-4876-b2ae-0bcc50d99b4b", 3 | "label": "Encyclopedia of China 3rd", 4 | "creator": "pixiandouban, jiaojiaoduabi", 5 | "target": "^https?://www\\.zgbk\\.com", 6 | "minVersion": "2.1.9", 7 | "maxVersion": "", 8 | "priority": 100, 9 | "inRepository": true, 10 | "translatorType": 4, 11 | "browserSupport": "gcsibv", 12 | "lastUpdated": "2024-06-20 16:34:51" 13 | } 14 | 15 | /* 16 | ***** BEGIN LICENSE BLOCK ***** 17 | 18 | Copyright © 2022 pixiandouban, jiaojiaodubai 19 | 20 | This file is part of Zotero. 21 | 22 | Zotero is free software: you can redistribute it and/or modify 23 | it under the terms of the GNU Affero General Public License as published by 24 | the Free Software Foundation, either version 3 of the License, or 25 | (at your option) any later version. 26 | 27 | Zotero is distributed in the hope that it will be useful, 28 | but WITHOUT ANY WARRANTY; without even the implied warranty of 29 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 30 | GNU Affero General Public License for more details. 31 | 32 | You should have received a copy of the GNU Affero General Public License 33 | along with Zotero. If not, see . 34 | 35 | ***** END LICENSE BLOCK ***** 36 | */ 37 | 38 | 39 | function detectWeb(doc, url) { 40 | if (url.includes('/ecph/words?')) { 41 | return 'encyclopediaArticle'; 42 | } 43 | else if (getSearchResults(doc, true)) { 44 | return 'multiple'; 45 | } 46 | return false; 47 | } 48 | 49 | function getSearchResults(doc, checkOnly) { 50 | var items = {}; 51 | var found = false; 52 | // .name见于首页推荐 53 | // h2见于搜索页 54 | var rows = doc.querySelectorAll('a[href*="/ecph/words"].name, h2 > a[href*="/ecph/words"]'); 55 | for (let row of rows) { 56 | let href = row.href; 57 | let title = ZU.trimInternal(row.textContent); 58 | if (!href || !title) continue; 59 | if (checkOnly) return true; 60 | found = true; 61 | items[href] = title; 62 | } 63 | return found ? items : false; 64 | } 65 | 66 | async function doWeb(doc, url) { 67 | if (detectWeb(doc, url) == 'multiple') { 68 | let items = await Zotero.selectItems(getSearchResults(doc, false)); 69 | if (!items) return; 70 | for (let url of Object.keys(items)) { 71 | await scrape(await requestDocument(url)); 72 | } 73 | } 74 | else { 75 | await scrape(doc, url); 76 | } 77 | } 78 | 79 | async function scrape(doc, url = doc.location.href) { 80 | var newItem = new Zotero.Item("encyclopediaArticle"); 81 | newItem.extra = ''; 82 | newItem.title = text(doc, '.title > h2'); 83 | newItem.abstractNote = text(doc, '.summary'); 84 | newItem.encyclopediaTitle = '中国大百科全书'; 85 | newItem.edition = '第三版·网络版'; 86 | newItem.publisher = '中国大百科全书出版社'; 87 | newItem.date = text(doc, '.time > span'); 88 | newItem.url = url; 89 | newItem.url = url; 90 | newItem.libraryCatalog = '中国大百科全书'; 91 | newItem.extra += addExtra('original-title', text(doc, '.enname').replace(/\//g, '')); 92 | // ".authorname .n-author > span"见于https://www.zgbk.com/ecph/words?SiteID=1&ID=456852&Type=bkzyb&SubID=99947 93 | doc.querySelectorAll('.author-noshadow .author-span > span, .authorname .n-author > span').forEach((element) => { 94 | let creator = element.innerText.replace(/(撰|修订)$/, ''); 95 | creator = ZU.cleanAuthor(creator, 'author'); 96 | if (/[\u4e00-\u9fa5]/.test(creator.lastName)) { 97 | creator.lastName = creator.firstName + creator.lastName; 98 | creator.firstName = ''; 99 | creator.fieldMode = 1; 100 | } 101 | newItem.creators.push(creator); 102 | }); 103 | newItem.language = 'zh-CN'; 104 | newItem.attachments.push({ 105 | title: 'Snapshot', 106 | document: doc 107 | }); 108 | newItem.complete(); 109 | } 110 | 111 | function addExtra(key, value) { 112 | return value 113 | ? `${key}: ${value}\n` 114 | : ''; 115 | } 116 | 117 | /** BEGIN TEST CASES **/ 118 | var testCases = [ 119 | { 120 | "type": "web", 121 | "url": "https://www.zgbk.com/ecph/words?SiteID=1&ID=86055", 122 | "items": [ 123 | { 124 | "itemType": "encyclopediaArticle", 125 | "title": "章锡琛", 126 | "creators": [ 127 | { 128 | "firstName": "", 129 | "lastName": "钟仁", 130 | "creatorType": "author", 131 | "fieldMode": 1 132 | }, 133 | { 134 | "firstName": "", 135 | "lastName": "张文彦", 136 | "creatorType": "author", 137 | "fieldMode": 1 138 | } 139 | ], 140 | "date": "2023-07-05", 141 | "abstractNote": "(1889-08-27~1969-06-06)\n中国编辑出版家。别名雪村。", 142 | "edition": "第三版·网络版", 143 | "encyclopediaTitle": "中国大百科全书", 144 | "extra": "original-title: Zhang Xichen", 145 | "language": "zh-CN", 146 | "libraryCatalog": "中国大百科全书", 147 | "publisher": "中国大百科全书出版社", 148 | "url": "https://www.zgbk.com/ecph/words?SiteID=1&ID=86055", 149 | "attachments": [ 150 | { 151 | "title": "Snapshot", 152 | "mimeType": "text/html" 153 | } 154 | ], 155 | "tags": [], 156 | "notes": [], 157 | "seeAlso": [] 158 | } 159 | ] 160 | }, 161 | { 162 | "type": "web", 163 | "url": "https://www.zgbk.com/ecph/search/result?SiteID=1&Alias=all&Query=%E5%AD%94%E5%AD%90", 164 | "items": "multiple" 165 | }, 166 | { 167 | "type": "web", 168 | "url": "https://www.zgbk.com/", 169 | "items": "multiple" 170 | }, 171 | { 172 | "type": "web", 173 | "url": "https://www.zgbk.com/ecph/subject?SiteID=1&ID=42424", 174 | "items": "multiple" 175 | } 176 | ] 177 | /** END TEST CASES **/ 178 | -------------------------------------------------------------------------------- /Jikan Full Text Database.js: -------------------------------------------------------------------------------- 1 | { 2 | "translatorID": "a8eaa2c8-4ed6-44f5-8532-b7e8ad8aeb83", 3 | "label": "Jikan Full Text Database", 4 | "creator": "jiaojiaodubai", 5 | "target": "^https://www\\.jikan\\.com\\.cn", 6 | "minVersion": "5.0", 7 | "maxVersion": "", 8 | "priority": 100, 9 | "inRepository": true, 10 | "translatorType": 4, 11 | "browserSupport": "gcsibv", 12 | "lastUpdated": "2024-11-06 20:43:54" 13 | } 14 | 15 | /* 16 | ***** BEGIN LICENSE BLOCK ***** 17 | 18 | Copyright © 2024 jiaojiaodubai 19 | 20 | This file is part of Zotero. 21 | 22 | Zotero is free software: you can redistribute it and/or modify 23 | it under the terms of the GNU Affero General Public License as published by 24 | the Free Software Foundation, either version 3 of the License, or 25 | (at your option) any later version. 26 | 27 | Zotero is distributed in the hope that it will be useful, 28 | but WITHOUT ANY WARRANTY; without even the implied warranty of 29 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 30 | GNU Affero General Public License for more details. 31 | 32 | You should have received a copy of the GNU Affero General Public License 33 | along with Zotero. If not, see . 34 | 35 | ***** END LICENSE BLOCK ***** 36 | */ 37 | 38 | 39 | function detectWeb(doc, url) { 40 | // abbreviation of "article detail" 41 | if (url.includes('/aD/')) { 42 | return 'bookSection'; 43 | } 44 | return false; 45 | } 46 | 47 | async function doWeb(doc, url) { 48 | const sectionRespond = await requestJSON(`https://www.jikan.com.cn/admin/api/article/detail?${tryMatch(url, /\bid=[^&]+/)}`); 49 | const sectionProxy = genProxy(sectionRespond.data); 50 | const newItem = new Z.Item('bookSection'); 51 | newItem.title = sectionProxy.title; 52 | newItem.abstractNote = sectionProxy.abstractCn; 53 | newItem.bookTitle = sectionProxy.collectedPapersName; 54 | newItem.volume = sectionProxy.collectnum; 55 | newItem.date = sectionProxy.releaseDate; 56 | newItem.pages = sectionProxy.bookNums; 57 | newItem.language = 'zh-CN'; 58 | newItem.url = `https://www.jikan.com.cn/aD/a?${tryMatch(url, /\bid=[^&]+/)}`; 59 | newItem.libraryCatalog = '集刊全文数据库'; 60 | const names = sectionProxy.author.split(' '); 61 | const creatorsExt = []; 62 | for (const name of names) { 63 | const creator = cleanAuthor(name.replace(/^〔.+?〕/, ''), 'author'); 64 | newItem.creators.push(JSON.parse(JSON.stringify(creator))); 65 | creator.country = tryMatch(name, /^〔(.+?)〕/, 1); 66 | creatorsExt.push(creator); 67 | } 68 | if (creatorsExt.some(creator => creator.country)) { 69 | newItem.extra = `creatorsExt: ${JSON.stringify(creatorsExt)}`; 70 | } 71 | doc.querySelectorAll('.keywords-wrap > p').forEach(elm => newItem.tags.push(elm.textContent)); 72 | try { 73 | const bookRespond = await requestJSON(`https://www.jikan.com.cn/admin/api/book/detail?id=${sectionProxy.bookId}`); 74 | const bookProxy = genProxy(bookRespond.data); 75 | newItem.publisher = bookProxy.publisher; 76 | newItem.ISBN = ZU.cleanISBN(bookProxy.isbn); 77 | bookProxy.author.split(' ').forEach(name => newItem.creators.push(cleanAuthor(name, 'editor'))); 78 | } 79 | catch (error) { 80 | Z.debug(error); 81 | } 82 | newItem.complete(); 83 | } 84 | 85 | function genProxy(raw) { 86 | const handler = { 87 | get(target, prop) { 88 | const value = target[prop]; 89 | return value === null 90 | ? '' 91 | : value; 92 | }, 93 | }; 94 | return new Proxy(raw, handler); 95 | } 96 | 97 | function tryMatch(string, pattern, index = 0) { 98 | if (!string) return ''; 99 | const match = string.match(pattern); 100 | return (match && match[index]) 101 | ? match[index] 102 | : ''; 103 | } 104 | 105 | 106 | function cleanAuthor(creator, creatorType = 'author') { 107 | creator = ZU.cleanAuthor(creator, creatorType); 108 | if (/[\u4e00-\u9fa5]/.test(creator.lastName)) { 109 | creator.lastName = creator.lastName.replace(/\.\s*/g, '. '); 110 | creator.fieldMode = 1; 111 | } 112 | return creator; 113 | } 114 | 115 | /** BEGIN TEST CASES **/ 116 | var testCases = [ 117 | { 118 | "type": "web", 119 | "url": "https://www.jikan.com.cn/aD/a?id=2703546", 120 | "items": [ 121 | { 122 | "itemType": "bookSection", 123 | "title": "关于民族志、历史及法律的几点思考", 124 | "creators": [ 125 | { 126 | "firstName": "", 127 | "lastName": "劳伦斯·M. 弗里德曼", 128 | "creatorType": "author", 129 | "fieldMode": 1 130 | }, 131 | { 132 | "firstName": "", 133 | "lastName": "王伟臣", 134 | "creatorType": "author", 135 | "fieldMode": 1 136 | }, 137 | { 138 | "firstName": "", 139 | "lastName": "吴婷", 140 | "creatorType": "author", 141 | "fieldMode": 1 142 | }, 143 | { 144 | "firstName": "", 145 | "lastName": "里赞", 146 | "creatorType": "editor", 147 | "fieldMode": 1 148 | }, 149 | { 150 | "firstName": "", 151 | "lastName": "刘昕杰", 152 | "creatorType": "editor", 153 | "fieldMode": 1 154 | } 155 | ], 156 | "date": "2021-10-01", 157 | "ISBN": "9787520192743", 158 | "abstractNote": "古典民族志有两大贡献,一是局外人的视角,二是文化相对性的论断。除此之外,民族志还是一种观察研究对象的技术或方法,可以弥补定量研究的缺陷。几乎关于人类社会的每一项重要研究都使用或暗含了民族志的研究方法。司法档案确实说明了一些案件事实,但我们必须学会批判式地解读。研究人员应该成为这些档案文件语言的解读者。档案研究、历史研究、司法档案的解读分析,归根结底都是民族志研究。", 159 | "bookTitle": "法律史评论", 160 | "extra": "creatorsExt: [{\"firstName\":\"\",\"lastName\":\"劳伦斯·M. 弗里德曼\",\"creatorType\":\"author\",\"fieldMode\":1,\"country\":\"美\"},{\"firstName\":\"\",\"lastName\":\"王伟臣\",\"creatorType\":\"author\",\"fieldMode\":1,\"country\":\"\"},{\"firstName\":\"\",\"lastName\":\"吴婷\",\"creatorType\":\"author\",\"fieldMode\":1,\"country\":\"\"}]", 161 | "language": "zh-CN", 162 | "libraryCatalog": "集刊全文数据库", 163 | "pages": "181-186", 164 | "publisher": "社会科学文献出版社", 165 | "url": "https://www.jikan.com.cn/aD/a?id=2703546", 166 | "volume": "17", 167 | "attachments": [], 168 | "tags": [ 169 | { 170 | "tag": "司法档案" 171 | }, 172 | { 173 | "tag": "定性研究" 174 | }, 175 | { 176 | "tag": "民族志" 177 | }, 178 | { 179 | "tag": "法律史" 180 | } 181 | ], 182 | "notes": [], 183 | "seeAlso": [] 184 | } 185 | ] 186 | } 187 | ] 188 | /** END TEST CASES **/ 189 | -------------------------------------------------------------------------------- /National Science and Technology Report Service - China.js: -------------------------------------------------------------------------------- 1 | { 2 | "translatorID": "550aaf0f-95ba-4ec1-a10d-d5b89e7036af", 3 | "label": "National Science and Technology Report Service - China", 4 | "creator": "jiaojiaodubai", 5 | "target": "^https://www\\.nstrs\\.cn", 6 | "minVersion": "5.0", 7 | "maxVersion": "", 8 | "priority": 100, 9 | "inRepository": true, 10 | "translatorType": 4, 11 | "browserSupport": "gcsibv", 12 | "lastUpdated": "2024-04-06 11:30:31" 13 | } 14 | 15 | /* 16 | ***** BEGIN LICENSE BLOCK ***** 17 | 18 | Copyright © 2024 jiaojiaodubai 19 | 20 | This file is part of Zotero. 21 | 22 | Zotero is free software: you can redistribute it and/or modify 23 | it under the terms of the GNU Affero General Public License as published by 24 | the Free Software Foundation, either version 3 of the License, or 25 | (at your option) any later version. 26 | 27 | Zotero is distributed in the hope that it will be useful, 28 | but WITHOUT ANY WARRANTY; without even the implied warranty of 29 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 30 | GNU Affero General Public License for more details. 31 | 32 | You should have received a copy of the GNU Affero General Public License 33 | along with Zotero. If not, see . 34 | 35 | ***** END LICENSE BLOCK ***** 36 | */ 37 | 38 | 39 | function detectWeb(doc, url) { 40 | let result = doc.querySelector('#Result'); 41 | if (result) { 42 | Z.monitorDOMChanges(result, { childList: true, subtree: true }); 43 | } 44 | if (url.includes('/detail?')) { 45 | return 'report'; 46 | } 47 | else if (getSearchResults(doc, true)) { 48 | return 'multiple'; 49 | } 50 | return false; 51 | } 52 | 53 | function getSearchResults(doc, checkOnly) { 54 | var items = {}; 55 | var found = false; 56 | // https://www.nstrs.cn/kjbg/navigation 57 | // https://www.nstrs.cn/kjbg/SearchResult?wd=%E7%94%B5%E6%9E%81 58 | var rows = doc.querySelectorAll('#Result tr a.shengle, #Result .BaoGao a.title'); 59 | for (let row of rows) { 60 | let href = row.href; 61 | let title = ZU.trimInternal(row.textContent); 62 | if (!href || !title) continue; 63 | if (checkOnly) return true; 64 | found = true; 65 | items[href] = title; 66 | } 67 | return found ? items : false; 68 | } 69 | 70 | async function doWeb(doc, url) { 71 | if (detectWeb(doc, url) == 'multiple') { 72 | let items = await Zotero.selectItems(getSearchResults(doc, false)); 73 | if (!items) return; 74 | for (let url of Object.keys(items)) { 75 | await scrape(doc, url); 76 | } 77 | } 78 | else { 79 | await scrape(doc, url); 80 | } 81 | } 82 | 83 | async function scrape(doc, url) { 84 | let id = url.match(/id=([^#/]+)/)[1]; 85 | let respond = await requestJSON('https://www.nstrs.cn/rest/kjbg/wfKjbg/getFilde', { 86 | method: 'POST', 87 | body: `id=${id}` 88 | }); 89 | if (respond.CODE == 0) { 90 | respond = respond.RESULT; 91 | let handler = { 92 | get(target, prop) { 93 | let value = target[prop]; 94 | return value 95 | ? value 96 | : ''; 97 | }, 98 | }; 99 | let proxy = new Proxy(respond, handler); 100 | let newItem = new Z.Item('report'); 101 | let extra = new Extra(); 102 | newItem.title = proxy.title; 103 | extra.set('original-title', ZU.capitalizeTitle(proxy.alternativeTitle), true); 104 | newItem.abstractNote = proxy.abstractCn; 105 | newItem.reportNumber = id; 106 | let period = proxy.kjbgType; 107 | newItem.reportType = '科技报告' + (period ? `(${period})` : ''); 108 | newItem.place = proxy.kjbgRegion; 109 | newItem.institution = proxy.prepareOrganization; 110 | newItem.date = ZU.strToISO(proxy.createTime); 111 | newItem.url = `https://www.nstrs.cn/kjbg/detail?id=${id}`; 112 | extra.set('program', proxy.projectName); 113 | extra.set('project', proxy.projectSubjectName); 114 | extra.set('projectNumber', proxy.projectSubjectId); 115 | extra.set('correspondingAuthor', [ 116 | proxy.linkmanName, 117 | proxy.linkmanAddresss, 118 | proxy.linkmanEmail, 119 | proxy.lnkmanPhone 120 | ].filter(s => s).join(',')); 121 | proxy.creator.split(';').forEach((creator) => { 122 | newItem.creators.push({ 123 | firstName: '', 124 | lastName: creator, 125 | creatorType: 'author', 126 | fieldMode: 1 127 | }); 128 | }); 129 | newItem.tags = proxy.keywordsCn.split(';'); 130 | let pdfLink = proxy.kjbgQWAddress; 131 | Z.debug(pdfLink); 132 | if (pdfLink) { 133 | newItem.attachments.push({ 134 | url: pdfLink, 135 | title: 'Full Text PDF', 136 | mimeType: 'application/pdf' 137 | }); 138 | } 139 | newItem.attachments.push({ 140 | title: 'Snapshot', 141 | document: doc 142 | }); 143 | newItem.extra = extra.toString(); 144 | newItem.complete(); 145 | } 146 | else { 147 | Z.debug(respond.MESSAGE); 148 | } 149 | } 150 | 151 | class Extra { 152 | constructor() { 153 | this.fields = []; 154 | } 155 | 156 | push(key, val, csl = false) { 157 | this.fields.push({ key: key, val: val, csl: csl }); 158 | } 159 | 160 | set(key, val, csl = false) { 161 | let target = this.fields.find(obj => new RegExp(`^${key}$`, 'i').test(obj.key)); 162 | if (target) { 163 | target.val = val; 164 | } 165 | else { 166 | this.push(key, val, csl); 167 | } 168 | } 169 | 170 | get(key) { 171 | let result = this.fields.find(obj => new RegExp(`^${key}$`, 'i').test(obj.key)); 172 | return result 173 | ? result.val 174 | : ''; 175 | } 176 | 177 | toString(history = '') { 178 | this.fields = this.fields.filter(obj => obj.val); 179 | return [ 180 | this.fields.filter(obj => obj.csl).map(obj => `${obj.key}: ${obj.val}`).join('\n'), 181 | history, 182 | this.fields.filter(obj => !obj.csl).map(obj => `${obj.key}: ${obj.val}`).join('\n') 183 | ].filter(obj => obj).join('\n'); 184 | } 185 | } 186 | 187 | /** BEGIN TEST CASES **/ 188 | var testCases = [ 189 | { 190 | "type": "web", 191 | "url": "https://www.nstrs.cn/kjbg/detail?id=A7C478B8-97AB-4D05-9633-42222335D03A", 192 | "defer": true, 193 | "items": [ 194 | { 195 | "itemType": "report", 196 | "title": "飞腾多核处理器芯层次化片上存储结构研究", 197 | "creators": [ 198 | { 199 | "firstName": "", 200 | "lastName": "周宏伟", 201 | "creatorType": "author", 202 | "fieldMode": 1 203 | }, 204 | { 205 | "firstName": "", 206 | "lastName": "邓让钰", 207 | "creatorType": "author", 208 | "fieldMode": 1 209 | }, 210 | { 211 | "firstName": "", 212 | "lastName": "曾坤", 213 | "creatorType": "author", 214 | "fieldMode": 1 215 | }, 216 | { 217 | "firstName": "", 218 | "lastName": "冯权友", 219 | "creatorType": "author", 220 | "fieldMode": 1 221 | }, 222 | { 223 | "firstName": "", 224 | "lastName": "杨乾明", 225 | "creatorType": "author", 226 | "fieldMode": 1 227 | } 228 | ], 229 | "date": "2017-11-19", 230 | "abstractNote": "众多的处理器核资源对访存带宽提出了很高的要求,而且通信和访存距离的增大无可避免。针对飞腾64核处理器访存带宽需求,本研究提出了基于片外存控的层次化存储结构和基于片内存控的层次化存储结构,并对其中的关键问题进行了深入研究并给出了相应的解决方案。提出的层次化片上存储结构提供对数据局部性优化机制的支持,能够减少线程之间的全局通信,结合片上数据移动和迁移机制能够进一步优化全局通信延迟和能效。", 231 | "extra": "original-title: Research on the hierarchical on-chip memory structure for the Phytium multi-core processor\nprogram: 国家科技重大专项\nproject: 超级计算机处理器研发\nprojectNumber: 2015ZX01028101\ncorrespondingAuthor: 窦强,中国人民解放军国防科技大学,douq@vip.sina.com", 232 | "institution": "中国人民解放军国防科技大学", 233 | "libraryCatalog": "National Science and Technology Report Service - China", 234 | "place": "其他", 235 | "reportNumber": "A7C478B8-97AB-4D05-9633-42222335D03A", 236 | "reportType": "科技报告(年度报告)", 237 | "url": "https://www.nstrs.cn/kjbg/detail?id=A7C478B8-97AB-4D05-9633-42222335D03A", 238 | "attachments": [ 239 | { 240 | "title": "Full Text PDF", 241 | "mimeType": "application/pdf" 242 | }, 243 | { 244 | "title": "Snapshot", 245 | "mimeType": "text/html" 246 | } 247 | ], 248 | "tags": [ 249 | { 250 | "tag": "处理器;层次化;存储" 251 | } 252 | ], 253 | "notes": [], 254 | "seeAlso": [] 255 | } 256 | ] 257 | }, 258 | { 259 | "type": "web", 260 | "url": "https://www.nstrs.cn/kjbg/navigation", 261 | "defer": true, 262 | "items": "multiple" 263 | }, 264 | { 265 | "type": "web", 266 | "url": "https://www.nstrs.cn/kjbg/SearchResult?wd=%E7%94%B5%E6%9E%81&q=TM:%25E7%2594%25B5%25E6%259E%2581", 267 | "defer": true, 268 | "items": "multiple" 269 | } 270 | ] 271 | /** END TEST CASES **/ 272 | -------------------------------------------------------------------------------- /People's Daily Database.js: -------------------------------------------------------------------------------- 1 | { 2 | "translatorID": "dbc3b499-88b6-4661-88c0-c27ac57ccd59", 3 | "label": "People's Daily Database", 4 | "creator": "pixiandouban", 5 | "target": "^https?://data\\.people\\.com\\.cn/rmrb", 6 | "minVersion": "5.0", 7 | "maxVersion": "", 8 | "priority": 100, 9 | "inRepository": true, 10 | "translatorType": 4, 11 | "browserSupport": "gcsibv", 12 | "lastUpdated": "2024-06-20 16:34:51" 13 | } 14 | 15 | /* 16 | ***** BEGIN LICENSE BLOCK ***** 17 | 18 | Copyright © 2023 pixiandouban 19 | 20 | This file is part of Zotero. 21 | 22 | Zotero is free software: you can redistribute it and/or modify 23 | it under the terms of the GNU Affero General Public License as published by 24 | the Free Software Foundation, either version 3 of the License, or 25 | (at your option) any later version. 26 | 27 | Zotero is distributed in the hope that it will be useful, 28 | but WITHOUT ANY WARRANTY; without even the implied warranty of 29 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 30 | GNU Affero General Public License for more details. 31 | 32 | You should have received a copy of the GNU Affero General Public License 33 | along with Zotero. If not, see . 34 | 35 | ***** END LICENSE BLOCK ***** 36 | */ 37 | 38 | function detectWeb(doc, url) { 39 | let box = doc.querySelector('#colorbox'); 40 | if (box) { 41 | Z.monitorDOMChanges(box, { childList: true, subtree: true }); 42 | } 43 | let lists = doc.querySelector('.title_list, .daodu_warp'); 44 | if (doc.querySelector('.rmrb_detail_pop')) { 45 | return 'newspaperArticle'; 46 | } 47 | else if (url.includes('qs') || lists) { 48 | return 'multiple'; 49 | } 50 | return false; 51 | } 52 | 53 | function getSearchResults(doc, checkOnly) { 54 | var items = {}; 55 | var found = false; 56 | var rows = doc.querySelectorAll('.title_list a, .daodu_warp a, .sreach_li a.open_detail_link'); 57 | for (let row of rows) { 58 | let href = row.href; 59 | let title = ZU.trimInternal(row.textContent); 60 | if (!href || !title) continue; 61 | if (checkOnly) return true; 62 | found = true; 63 | items[href] = title; 64 | } 65 | return found ? items : false; 66 | } 67 | 68 | async function doWeb(doc, url) { 69 | if (detectWeb(doc, url) == 'multiple') { 70 | let items = await Zotero.selectItems(getSearchResults(doc, false)); 71 | if (!items) return; 72 | for (let url of Object.keys(items)) { 73 | await scrape(await requestDocument(url)); 74 | } 75 | } 76 | else { 77 | await scrape(doc, url); 78 | } 79 | } 80 | 81 | async function scrape(doc, url = doc.location.href) { 82 | var newItem = new Zotero.Item('newspaperArticle'); 83 | newItem.title = text(doc, 'div.title'); 84 | newItem.publicationTitle = '人民日报'; 85 | let subTitle = text(doc, 'div.subtitle'); 86 | if (subTitle) { 87 | newItem.shortTitle = newItem.title; 88 | newItem.title += subTitle; 89 | } 90 | newItem.date = text(doc, 'div.sha_left span:nth-child(1)'); 91 | newItem.pages = text(doc, 'div.sha_left span:nth-child(2)'); 92 | newItem.language = 'zh-CN'; 93 | newItem.ISSN = '1672-8386'; 94 | newItem.url = url; 95 | text(doc, 'div.author').replace(/^【?本报记者 ?|】$/g, '').split(/[,、\s;]+/) 96 | .forEach((creator) => { 97 | creator = ZU.cleanAuthor(creator, 'author'); 98 | creator.fieldMode = 1; 99 | newItem.creators.push(creator); 100 | }); 101 | newItem.attachments.push({ 102 | title: 'Snapshot', 103 | document: doc 104 | }); 105 | newItem.complete(); 106 | } 107 | 108 | /** BEGIN TEST CASES **/ 109 | var testCases = [ 110 | { 111 | "type": "web", 112 | "url": "http://data.people.com.cn/rmrb/20231231/1?code=2", 113 | "items": "multiple" 114 | }, 115 | { 116 | "type": "web", 117 | "url": "http://data.people.com.cn/rmrb/20231231/1/32924b7882d94d80ad59562d52280fbb", 118 | "items": [ 119 | { 120 | "itemType": "newspaperArticle", 121 | "title": "高高举起构建人类命运共同体光辉旗帜——二论贯彻落实中央外事工作会议精神", 122 | "creators": [ 123 | { 124 | "firstName": "", 125 | "lastName": "本报评论员", 126 | "creatorType": "author", 127 | "fieldMode": 1 128 | } 129 | ], 130 | "date": "2023-12-31", 131 | "ISSN": "1672-8386", 132 | "language": "zh-CN", 133 | "libraryCatalog": "People's Daily", 134 | "pages": "1", 135 | "publicationTitle": "人民日报", 136 | "shortTitle": "高高举起构建人类命运共同体光辉旗帜", 137 | "url": "http://data.people.com.cn/rmrb/20231231/1/32924b7882d94d80ad59562d52280fbb", 138 | "attachments": [ 139 | { 140 | "title": "Snapshot", 141 | "mimeType": "text/html" 142 | } 143 | ], 144 | "tags": [], 145 | "notes": [], 146 | "seeAlso": [] 147 | } 148 | ] 149 | }, 150 | { 151 | "type": "web", 152 | "url": "http://data.people.com.cn/rmrb/20240620/1/24c0fd81008447179e4e53127ac959d2", 153 | "items": [ 154 | { 155 | "itemType": "newspaperArticle", 156 | "title": "学党史 明党纪党纪学习教育开展以来,中国共产党历史展览馆充分发挥党史展览的教育功能——", 157 | "creators": [ 158 | { 159 | "firstName": "", 160 | "lastName": "李林蔚", 161 | "creatorType": "author", 162 | "fieldMode": 1 163 | } 164 | ], 165 | "date": "2024-06-20", 166 | "ISSN": "1672-8386", 167 | "language": "zh-CN", 168 | "libraryCatalog": "People's Daily Database", 169 | "pages": "1", 170 | "publicationTitle": "人民日报", 171 | "shortTitle": "学党史 明党纪", 172 | "url": "http://data.people.com.cn/rmrb/20240620/1/24c0fd81008447179e4e53127ac959d2", 173 | "attachments": [ 174 | { 175 | "title": "Snapshot", 176 | "mimeType": "text/html" 177 | } 178 | ], 179 | "tags": [], 180 | "notes": [], 181 | "seeAlso": [] 182 | } 183 | ] 184 | } 185 | ] 186 | /** END TEST CASES **/ 187 | -------------------------------------------------------------------------------- /People's Daily Epaper.js: -------------------------------------------------------------------------------- 1 | { 2 | "translatorID": "84bd7e84-c61d-4348-8615-3b56d9ebb848", 3 | "label": "People's Daily Epaper", 4 | "creator": "jiaojiaodubai", 5 | "target": "^http://paper\\.people(\\.com)?\\.cn", 6 | "minVersion": "5.0", 7 | "maxVersion": "", 8 | "priority": 100, 9 | "inRepository": true, 10 | "translatorType": 4, 11 | "browserSupport": "gcsibv", 12 | "lastUpdated": "2024-06-09 13:28:34" 13 | } 14 | 15 | /* 16 | ***** BEGIN LICENSE BLOCK ***** 17 | 18 | Copyright © 2022 jiaojiaodubai 19 | 20 | This file is part of Zotero. 21 | 22 | Zotero is free software: you can redistribute it and/or modify 23 | it under the terms of the GNU Affero General Public License as published by 24 | the Free Software Foundation, either version 3 of the License, or 25 | (at your option) any later version. 26 | 27 | Zotero is distributed in the hope that it will be useful, 28 | but WITHOUT ANY WARRANTY; without even the implied warranty of 29 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 30 | GNU Affero General Public License for more details. 31 | 32 | You should have received a copy of the GNU Affero General Public License 33 | along with Zotero. If not, see . 34 | 35 | ***** END LICENSE BLOCK ***** 36 | */ 37 | 38 | const newspaperMap = { 39 | rmrb: '人民日报', 40 | rmrbhwb: '人民日报海外版', 41 | zgnyb: '中国能源报', 42 | fcyym: '讽刺与幽默', 43 | zgcsb: '中国城市报', 44 | }; 45 | 46 | const journalMap = { 47 | xwzx: '新闻战线', 48 | rmlt: '人民论坛', 49 | rmzk: '人民周刊', 50 | zgjjzk: '中国经济周刊', 51 | mszk: '民生周刊', 52 | zgby: '中国报业' 53 | }; 54 | function detectWeb(doc, url) { 55 | if (doc.querySelector('.article-box')) { 56 | if (Object.keys(newspaperMap).some(key => url.includes(`/${key}/`))) { 57 | return 'newspaperArticle'; 58 | } 59 | else if (Object.keys(journalMap).some(key => url.includes(`/${key}/`))) { 60 | return 'journalArticle'; 61 | } 62 | } 63 | else if (getSearchResults(doc, true)) { 64 | return 'multiple'; 65 | } 66 | return false; 67 | } 68 | 69 | function getSearchResults(doc, checkOnly) { 70 | var items = {}; 71 | var found = false; 72 | var rows = doc.querySelectorAll('.news > ul > li > a'); 73 | for (let row of rows) { 74 | let href = row.href; 75 | let title = ZU.trimInternal(row.textContent); 76 | if (!href || !title) continue; 77 | if (checkOnly) return true; 78 | found = true; 79 | items[href] = title; 80 | } 81 | return found ? items : false; 82 | } 83 | 84 | async function doWeb(doc, url) { 85 | if (detectWeb(doc, url) == 'multiple') { 86 | let items = await Zotero.selectItems(getSearchResults(doc, false)); 87 | if (!items) return; 88 | for (let url of Object.keys(items)) { 89 | await scrape(await requestDocument(url)); 90 | } 91 | } 92 | else { 93 | await scrape(doc, url); 94 | } 95 | } 96 | 97 | async function scrape(doc, url = doc.location.href) { 98 | var newItem = new Z.Item(detectWeb(doc, url)); 99 | newItem.title = text(doc, '.article > h3') + text(doc, '.article > h3+h1') + text(doc, '.article > h1+h2'); 100 | newItem.shortTitle = text(doc, '.article > h1'); 101 | let key = [...Object.keys(newspaperMap), ...Object.keys(journalMap)].find(key => url.includes(`/${key}/`)); 102 | newItem.publicationTitle = newspaperMap[key] || journalMap[key]; 103 | let pubInfo = innerText(doc, '.sec > .date').replace(/\s/g, ''); 104 | newItem.place = '北京'; 105 | newItem.date = tryMatch(pubInfo, /(\d+)年(\d+)月(\d+)日/).replace(/(\d+)年(\d+)月(\d+)日/, '$1-$2-$3'); 106 | newItem.pages = tryMatch(pubInfo, /第0*([1-9]\d*)版/, 1); 107 | pureText(doc.querySelector('.sec')) 108 | // 预先将二字人名拼接起来 109 | .replace(/□/, '') 110 | .replace(/([^\u4e00-\u9fa5][\u4e00-\u9fa5])\s+([\u4e00-\u9fa5](?:[^\u4e00-\u9fa5]|$))/g, '$1$2') 111 | .split(/\s/) 112 | .filter(creator => !creator.includes('记者')) 113 | .forEach((creator) => { 114 | creator = creator.replace(/(图|文)\|/, ''); 115 | creator = ZU.cleanAuthor(creator, 'author'); 116 | creator.fieldMode = 1; 117 | newItem.creators.push(creator); 118 | }); 119 | newItem.language = 'zh-CN'; 120 | newItem.url = url; 121 | newItem.libraryCatalog = '人民日报图文数据库'; 122 | newItem.attachments.push({ 123 | title: 'Snapshot', 124 | document: doc 125 | }); 126 | newItem.complete(); 127 | } 128 | 129 | function tryMatch(string, pattern, index = 0) { 130 | if (!string) return ''; 131 | let match = string.match(pattern); 132 | return (match && match[index]) 133 | ? match[index] 134 | : ''; 135 | } 136 | 137 | function pureText(element) { 138 | if (!element) return ''; 139 | // Deep copy to avoid affecting the original page. 140 | let elementCopy = element.cloneNode(true); 141 | while (elementCopy.lastElementChild) { 142 | elementCopy.removeChild(elementCopy.lastElementChild); 143 | } 144 | return ZU.trimInternal(elementCopy.innerText); 145 | } 146 | 147 | /** BEGIN TEST CASES **/ 148 | var testCases = [ 149 | { 150 | "type": "web", 151 | "url": "http://paper.people.com.cn/mszk/html/2023-12/18/content_26034010.htm", 152 | "items": [ 153 | { 154 | "itemType": "journalArticle", 155 | "title": "生态环境监管不能走过场", 156 | "creators": [ 157 | { 158 | "firstName": "", 159 | "lastName": "严碧华", 160 | "creatorType": "author", 161 | "fieldMode": 1 162 | } 163 | ], 164 | "date": "2023-12-18", 165 | "language": "zh-CN", 166 | "libraryCatalog": "人民日报图文数据库", 167 | "pages": "1", 168 | "publicationTitle": "民生周刊", 169 | "url": "http://paper.people.com.cn/mszk/html/2023-12/18/content_26034010.htm", 170 | "attachments": [ 171 | { 172 | "title": "Snapshot", 173 | "mimeType": "text/html" 174 | } 175 | ], 176 | "tags": [], 177 | "notes": [], 178 | "seeAlso": [] 179 | } 180 | ] 181 | }, 182 | { 183 | "type": "web", 184 | "url": "http://paper.people.com.cn/rmrb/html/2024-01/07/nw.D110000renmrb_20240107_1-01.htm", 185 | "items": [ 186 | { 187 | "itemType": "newspaperArticle", 188 | "title": "辽宁推动产业集群高质量发展", 189 | "creators": [ 190 | { 191 | "firstName": "", 192 | "lastName": "刘成友", 193 | "creatorType": "author", 194 | "fieldMode": 1 195 | }, 196 | { 197 | "firstName": "", 198 | "lastName": "刘佳华", 199 | "creatorType": "author", 200 | "fieldMode": 1 201 | } 202 | ], 203 | "date": "2024-01-07", 204 | "language": "zh-CN", 205 | "libraryCatalog": "人民日报图文数据库", 206 | "pages": "1", 207 | "place": "北京", 208 | "publicationTitle": "人民日报", 209 | "url": "http://paper.people.com.cn/rmrb/html/2024-01/07/nw.D110000renmrb_20240107_1-01.htm", 210 | "attachments": [ 211 | { 212 | "title": "Snapshot", 213 | "mimeType": "text/html" 214 | } 215 | ], 216 | "tags": [], 217 | "notes": [], 218 | "seeAlso": [] 219 | } 220 | ] 221 | }, 222 | { 223 | "type": "web", 224 | "url": "http://paper.people.com.cn/zgnyb/html/2024-01/01/node_2222.htm", 225 | "items": "multiple" 226 | }, 227 | { 228 | "type": "web", 229 | "url": "http://paper.people.com.cn/zgby/html/2022-01/25/node_2751.htm", 230 | "items": "multiple" 231 | }, 232 | { 233 | "type": "web", 234 | "url": "http://paper.people.com.cn/xwzx/html/2021-12/01/content_26016862.htm", 235 | "items": [ 236 | { 237 | "itemType": "journalArticle", 238 | "title": "融媒体时代深挖财经报道的主流价值——电视新闻专题《百亿大和解》创作感悟", 239 | "creators": [ 240 | { 241 | "firstName": "", 242 | "lastName": "原宝国", 243 | "creatorType": "author", 244 | "fieldMode": 1 245 | }, 246 | { 247 | "firstName": "", 248 | "lastName": "韩信", 249 | "creatorType": "author", 250 | "fieldMode": 1 251 | }, 252 | { 253 | "firstName": "", 254 | "lastName": "王兴涛", 255 | "creatorType": "author", 256 | "fieldMode": 1 257 | } 258 | ], 259 | "date": "2021-12-01", 260 | "language": "zh-CN", 261 | "libraryCatalog": "人民日报图文数据库", 262 | "pages": "3", 263 | "publicationTitle": "新闻战线", 264 | "shortTitle": "融媒体时代深挖财经报道的主流价值", 265 | "url": "http://paper.people.com.cn/xwzx/html/2021-12/01/content_26016862.htm", 266 | "attachments": [ 267 | { 268 | "title": "Snapshot", 269 | "mimeType": "text/html" 270 | } 271 | ], 272 | "tags": [], 273 | "notes": [], 274 | "seeAlso": [] 275 | } 276 | ] 277 | } 278 | ] 279 | /** END TEST CASES **/ 280 | -------------------------------------------------------------------------------- /QStheory.js: -------------------------------------------------------------------------------- 1 | { 2 | "translatorID": "85862c7a-5acd-410b-88a3-6d0fdf39479d", 3 | "label": "QStheory", 4 | "creator": "jiaojiaodubai", 5 | "target": "^http://www\\.qstheory\\.cn", 6 | "minVersion": "5.0", 7 | "maxVersion": "", 8 | "priority": 100, 9 | "inRepository": true, 10 | "translatorType": 4, 11 | "browserSupport": "gcsibv", 12 | "lastUpdated": "2024-01-09 06:41:46" 13 | } 14 | 15 | /* 16 | ***** BEGIN LICENSE BLOCK ***** 17 | 18 | Copyright © 2024 jiaojiaodubai 19 | 20 | This file is part of Zotero. 21 | 22 | Zotero is free software: you can redistribute it and/or modify 23 | it under the terms of the GNU Affero General Public License as published by 24 | the Free Software Foundation, either version 3 of the License, or 25 | (at your option) any later version. 26 | 27 | Zotero is distributed in the hope that it will be useful, 28 | but WITHOUT ANY WARRANTY; without even the implied warranty of 29 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 30 | GNU Affero General Public License for more details. 31 | 32 | You should have received a copy of the GNU Affero General Public License 33 | along with Zotero. If not, see . 34 | 35 | ***** END LICENSE BLOCK ***** 36 | */ 37 | 38 | 39 | function detectWeb(doc, url) { 40 | if (url.includes('/dukan/')) { 41 | if (doc.querySelector('p > strong > a')) { 42 | return 'multiple'; 43 | } 44 | return 'journalArticle'; 45 | } 46 | // 必须要有标题才能顺利保存条目 47 | else if (doc.querySelector('.inner > h1')) { 48 | return 'webpage'; 49 | } 50 | else if (getSearchResults(doc, true)) { 51 | return 'multiple'; 52 | } 53 | return false; 54 | } 55 | 56 | function getSearchResults(doc, checkOnly) { 57 | var items = {}; 58 | var found = false; 59 | var rows = Array.from(doc.querySelectorAll('a')).filter(element => new RegExp('/\\w+/\\d{4}-\\d{2}/\\d{2}/').test(element.href)); 60 | for (let row of rows) { 61 | let href = row.href; 62 | let title = ZU.trimInternal(row.textContent); 63 | if (!href || !title) continue; 64 | if (checkOnly) return true; 65 | found = true; 66 | items[href] = title; 67 | } 68 | return found ? items : false; 69 | } 70 | 71 | async function doWeb(doc, url) { 72 | if (detectWeb(doc, url) == 'multiple') { 73 | let items = await Zotero.selectItems(getSearchResults(doc, false)); 74 | if (!items) return; 75 | for (let url of Object.keys(items)) { 76 | await scrape(await requestDocument(url)); 77 | } 78 | } 79 | else { 80 | await scrape(doc, url); 81 | } 82 | } 83 | 84 | async function scrape(doc, url = doc.location.href) { 85 | var newItem = new Z.Item(detectWeb(doc, url)); 86 | newItem.title = text(doc, '.inner > h1') + text(doc, '.inner > h2'); 87 | newItem.shortTitle = text(doc, '.inner > h1'); 88 | switch (newItem.itemType) { 89 | case 'journalArticle': { 90 | let pubInfo = text(doc, '.appellation'); 91 | newItem.publicationTitle = tryMatch(pubInfo, /:《?(.+?)》?\d+/, 1); 92 | if (newItem.publicationTitle == '求是') { 93 | newItem.ISSN = '1002-4980'; 94 | } 95 | newItem.issue = tryMatch(pubInfo, /0*([1-9]\d*)$/, 1); 96 | newItem.date = tryMatch(pubInfo, /\d{4}/); 97 | break; 98 | } 99 | case 'webpage': 100 | newItem.websiteTitle = '求是网'; 101 | newItem.date = ZU.strToISO(text(doc, '.pubtime')); 102 | break; 103 | } 104 | newItem.language = 'zh-CN'; 105 | newItem.url = url; 106 | newItem.creators = [...processName(text(doc, '.appellation', 1), 'author'), ...processName(text(doc, '.pull-right'), 'contributor')]; 107 | newItem.attachments.push({ 108 | title: 'Snapshot', 109 | document: doc 110 | }); 111 | newItem.complete(); 112 | } 113 | 114 | function tryMatch(string, pattern, index = 0) { 115 | let match = string.match(pattern); 116 | if (match && match[index]) { 117 | return match[index]; 118 | } 119 | return ''; 120 | } 121 | 122 | function processName(creators, creatorType) { 123 | creators = creators 124 | .trim() 125 | .replace(/^[(([【[]|[))]】\]]$/g, '') 126 | .replace(/([^\u4e00-\u9fff][\u4e00-\u9fff])\s+([\u4e00-\u9fff](?:[^\u4e00-\u9fff]|$))/g, '$1$2') 127 | .split(/[(?:\s+)、::]/g) 128 | .filter(creator => !/(编|校对|审校|记者|作者|图|文|-)/.test(creator)); 129 | Z.debug(creators); 130 | creators = creators.map((creator) => { 131 | creator = ZU.cleanAuthor(creator, creatorType); 132 | creator.fieldMode = 1; 133 | return creator; 134 | }); 135 | return creators; 136 | } 137 | 138 | /** BEGIN TEST CASES **/ 139 | var testCases = [ 140 | { 141 | "type": "web", 142 | "url": "http://www.qstheory.cn/laigao/ycjx/2023-12/12/c_1130019251.htm", 143 | "items": [ 144 | { 145 | "itemType": "webpage", 146 | "title": "“枫桥经验”是来自人民群众的实践创造", 147 | "creators": [ 148 | { 149 | "firstName": "", 150 | "lastName": "是说新语", 151 | "creatorType": "author", 152 | "fieldMode": 1 153 | }, 154 | { 155 | "firstName": "", 156 | "lastName": "汤宝兰", 157 | "creatorType": "contributor", 158 | "fieldMode": 1 159 | } 160 | ], 161 | "date": "2023-12-12", 162 | "language": "zh-CN", 163 | "url": "http://www.qstheory.cn/laigao/ycjx/2023-12/12/c_1130019251.htm", 164 | "websiteTitle": "求是网", 165 | "attachments": [ 166 | { 167 | "title": "Snapshot", 168 | "mimeType": "text/html" 169 | } 170 | ], 171 | "tags": [], 172 | "notes": [], 173 | "seeAlso": [] 174 | } 175 | ] 176 | }, 177 | { 178 | "type": "web", 179 | "url": "http://www.qstheory.cn/dukan/qs/2023-01/01/c_1129246885.htm", 180 | "items": [ 181 | { 182 | "itemType": "journalArticle", 183 | "title": "铁人队伍永向前", 184 | "creators": [ 185 | { 186 | "firstName": "", 187 | "lastName": "周昭成", 188 | "creatorType": "author", 189 | "fieldMode": 1 190 | }, 191 | { 192 | "firstName": "", 193 | "lastName": "陈聪", 194 | "creatorType": "author", 195 | "fieldMode": 1 196 | }, 197 | { 198 | "firstName": "", 199 | "lastName": "张芯蕊", 200 | "creatorType": "contributor", 201 | "fieldMode": 1 202 | }, 203 | { 204 | "firstName": "", 205 | "lastName": "徐勇林", 206 | "creatorType": "contributor", 207 | "fieldMode": 1 208 | }, 209 | { 210 | "firstName": "", 211 | "lastName": "夏明月", 212 | "creatorType": "contributor", 213 | "fieldMode": 1 214 | }, 215 | { 216 | "firstName": "", 217 | "lastName": "陈嵘", 218 | "creatorType": "contributor", 219 | "fieldMode": 1 220 | } 221 | ], 222 | "date": "2023", 223 | "issue": "1", 224 | "language": "zh-CN", 225 | "libraryCatalog": "QStheory", 226 | "url": "http://www.qstheory.cn/dukan/qs/2023-01/01/c_1129246885.htm", 227 | "attachments": [ 228 | { 229 | "title": "Snapshot", 230 | "mimeType": "text/html" 231 | } 232 | ], 233 | "tags": [], 234 | "notes": [], 235 | "seeAlso": [] 236 | } 237 | ] 238 | }, 239 | { 240 | "type": "web", 241 | "url": "http://www.qstheory.cn/dukan/qs/2014/2023-01/01/c_1129247031.htm", 242 | "items": "multiple" 243 | }, 244 | { 245 | "type": "web", 246 | "url": "http://www.qstheory.cn/", 247 | "items": "multiple" 248 | } 249 | ] 250 | 251 | /** END TEST CASES **/ 252 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Zotero translators 中文仓库 2 | 3 | 目前 Zotero 中有许多抓取中文学术网站的转换器,但这些转换器有些已经非常老旧,缺少及时的维护。希望能在这里召集一些志同道合的朋友,共同维护中文学术或其他类型网站的 Zotero 转换器。 4 | 如果 Github 下载速度慢,可以试试 [Gitee](https://gitee.com/l0o0/translators_CN)。 5 | 6 | ## 📢 如何更新 7 | 8 | 👉 视频教程:[Zotero 更新知网Translator翻译器教程 - Bilibili](https://www.bilibili.com/video/BV1F54y1k73n) 9 | 👉 图文教程:[Zotero 百科全书](https://zotero-chinese.com/user-guide/faqs/update-translators.html) 10 | 👉 完整讨论:[从浏览器保存条目时发生错误 / 抓取时不能自动下载PDF / 无法自动给添加的PDF附件创建条目怎么办](https://gitee.com/zotero-chinese/zotero-chinese/issues/I56D62) 11 | 12 | 如果完成以上操作后仍未解决问题,请发布 issue 反馈问题: 13 | 14 | - [Github 反馈入口](https://github.com/l0o0/translators_CN/issues/new/choose)(首选) 15 | - [Gitee 反馈入口](https://gitee.com/l0o0/translators_CN/issues/new)(备用) 16 | 17 | ## 📄 参与贡献 18 | 19 | 在开始创建前,浏览下面这些材料可以帮你了解一些创建 translator 的基本知识和开发的工具。 20 | 21 | - [Zotero 文档教你写 translator](https://www.zotero.org/support/dev/translators/coding) 22 | - [Zotero JavaScript API](https://www.zotero.org/support/dev/client_coding/javascript_api) 23 | - [Translator 中可能用到的函数](https://www.zotero.org/support/dev/translators/functions) 24 | - [Wiki-Create translator](https://www.mediawiki.org/wiki/Citoid/Creating_Zotero_translators),了解基本HTML结构,CSS选择器,javascript基本语法等 25 | - [refworks 引文格式](./data/refworks.pdf),有些学术网站可以将引文导出为 refworks 格式 26 | - [Scaffold 使用说明](https://www.zotero.org/support/dev/translators/scaffold),官方出品,便于创建 translator 的工具 27 | - [MDN Javascript 中文教程](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/A_re-introduction_to_JavaScript) 28 | - [Zotero 条目类型说明](https://aurimasv.github.io/z2csl/typeMap.xml) 29 | - [How to write a Zotero translator](https://niche-canada.org/member-projects/zotero-guide/about.html) 30 | 31 | ## 🦸 其他热心参与者 32 | 33 | [@jiaojiaodubai](https://github.com/jiaojiaodubai) 34 | [@wanyzh](https://github.com/wanyzh) 35 | [@smilevent](https://github.com/smilevent) 36 | [@Lemmingh](https://github.com/Lemmingh) 37 | [@Captain2021 (啊哈船长)](https://github.com/Captain2021) 38 | [道格学社](https://github.com/gezhongran/DougSociety)及学员[Felix](https://github.com/xuwd)、[018](https://github.com/018) 39 | 40 | ## 🎈问题交流 41 | 42 | 如果有问题的,可以加QQ群 913637964,一起交流。 43 | -------------------------------------------------------------------------------- /SKCTK.js: -------------------------------------------------------------------------------- 1 | { 2 | "translatorID": "b9d2e5ab-1e1b-49c8-b1e7-7d26d35064d8", 3 | "label": "SKCTK", 4 | "creator": "jiaojiaodubai", 5 | "target": "^https://www\\.skctk\\.cn", 6 | "minVersion": "5.0", 7 | "maxVersion": "", 8 | "priority": 100, 9 | "inRepository": true, 10 | "translatorType": 4, 11 | "browserSupport": "gcsibv", 12 | "lastUpdated": "2024-01-06 08:16:18" 13 | } 14 | 15 | /* 16 | ***** BEGIN LICENSE BLOCK ***** 17 | 18 | Copyright © 2024 jiaojiaodubai 19 | 20 | This file is part of Zotero. 21 | 22 | Zotero is free software: you can redistribute it and/or modify 23 | it under the terms of the GNU Affero General Public License as published by 24 | the Free Software Foundation, either version 3 of the License, or 25 | (at your option) any later version. 26 | 27 | Zotero is distributed in the hope that it will be useful, 28 | but WITHOUT ANY WARRANTY; without even the implied warranty of 29 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 30 | GNU Affero General Public License for more details. 31 | 32 | You should have received a copy of the GNU Affero General Public License 33 | along with Zotero. If not, see . 34 | 35 | ***** END LICENSE BLOCK ***** 36 | */ 37 | 38 | 39 | function detectWeb(doc, url) { 40 | if (url.includes('/html/')) { 41 | return 'encyclopediaArticle'; 42 | } 43 | else if (getSearchResults(doc, true)) { 44 | return 'multiple'; 45 | } 46 | return false; 47 | } 48 | 49 | function getSearchResults(doc, checkOnly) { 50 | var items = {}; 51 | var found = false; 52 | var rows = doc.querySelectorAll('li > a[href*="/html/"], .title > a[href*="/html/"]'); 53 | for (let row of rows) { 54 | let href = row.href; 55 | let title = ZU.trimInternal(row.textContent); 56 | if (!href || !title) continue; 57 | if (checkOnly) return true; 58 | found = true; 59 | items[href] = title; 60 | } 61 | return found ? items : false; 62 | } 63 | 64 | async function doWeb(doc, url) { 65 | if (detectWeb(doc, url) == 'multiple') { 66 | let items = await Zotero.selectItems(getSearchResults(doc, false)); 67 | if (!items) return; 68 | for (let url of Object.keys(items)) { 69 | await scrape(await requestDocument(url)); 70 | } 71 | } 72 | else { 73 | await scrape(doc, url); 74 | } 75 | } 76 | 77 | async function scrape(doc, url = doc.location.href) { 78 | var newItem = new Z.Item('encyclopediaArticle'); 79 | newItem.extra = ''; 80 | newItem.title = text(doc, '#content > .title'); 81 | newItem.abstractNote = text(doc, '.intro .txt'); 82 | newItem.encyclopediaTitle = '中国社会科学词条库'; 83 | newItem.publisher = '中国大百科全书出版社'; 84 | newItem.date = text(doc, '.last_udpate'); 85 | newItem.url = tryMatch(url, /^.+?html.+?html/); 86 | newItem.language = 'zh-CN'; 87 | newItem.libraryCatalog = '中国社会科学词条库'; 88 | newItem.rights = '中国大百科全书出版社有限公司'; 89 | text(doc, '.author').replace(/^(作者:/, '').replace(/)$/, '') 90 | .split(/\s/) 91 | .forEach((creator) => { 92 | creator = creator.replace(/(撰|修订)$/, ''); 93 | creator = ZU.cleanAuthor(creator, 'author'); 94 | if (/[\u4e00-\u9fa5]/.test(creator.lastName)) { 95 | creator.lastName = creator.firstName + creator.lastName; 96 | creator.firstName = ''; 97 | creator.fieldMode = 1; 98 | } 99 | newItem.creators.push(creator); 100 | }); 101 | newItem.language = 'zh-CN'; 102 | newItem.attachments.push({ 103 | title: 'Snapshot', 104 | document: doc 105 | }); 106 | newItem.complete(); 107 | } 108 | 109 | 110 | function tryMatch(string, pattern, index = 0) { 111 | if (!string) return ''; 112 | let match = string.match(pattern); 113 | return (match && match[index]) 114 | ? match[index] 115 | : ''; 116 | } 117 | 118 | /** BEGIN TEST CASES **/ 119 | var testCases = [ 120 | { 121 | "type": "web", 122 | "url": "https://www.skctk.cn/html/1/1-177466-1.html?is_search=hot_search&t=1704526856", 123 | "items": [ 124 | { 125 | "itemType": "encyclopediaArticle", 126 | "title": "唯物主义", 127 | "creators": [ 128 | { 129 | "firstName": "", 130 | "lastName": "赵光武", 131 | "creatorType": "author", 132 | "fieldMode": 1 133 | }, 134 | { 135 | "firstName": "", 136 | "lastName": "谢地坤", 137 | "creatorType": "author", 138 | "fieldMode": 1 139 | } 140 | ], 141 | "date": "最后更新 2022-12-01", 142 | "abstractNote": "哲学上的两大基本派别之一;与唯心主义对立的理论体系。", 143 | "encyclopediaTitle": "中国社会科学词条库", 144 | "language": "zh-CN", 145 | "libraryCatalog": "中国社会科学词条库", 146 | "publisher": "中国大百科全书出版社", 147 | "rights": "中国大百科全书出版社有限公司", 148 | "url": "https://www.skctk.cn/html/1/1-177466-1.html", 149 | "attachments": [ 150 | { 151 | "title": "Snapshot", 152 | "mimeType": "text/html" 153 | } 154 | ], 155 | "tags": [], 156 | "notes": [], 157 | "seeAlso": [] 158 | } 159 | ] 160 | }, 161 | { 162 | "type": "web", 163 | "url": "https://www.skctk.cn/index.php?g=portal&m=search&a=index", 164 | "items": "multiple" 165 | }, 166 | { 167 | "type": "web", 168 | "url": "https://www.skctk.cn/index.php?m=list&a=hot", 169 | "items": "multiple" 170 | }, 171 | { 172 | "type": "web", 173 | "url": "https://www.skctk.cn/", 174 | "items": "multiple" 175 | } 176 | ] 177 | /** END TEST CASES **/ 178 | -------------------------------------------------------------------------------- /Sina Weibo.js: -------------------------------------------------------------------------------- 1 | { 2 | "translatorID": "231208c0-1a69-4f58-b0f7-4a78e5e057a5", 3 | "label": "Sina Weibo", 4 | "creator": "pixiandouban, jiaojiaodubai", 5 | "target": "^https?://.*weibo\\.com/", 6 | "minVersion": "4.0", 7 | "maxVersion": "", 8 | "priority": 100, 9 | "inRepository": true, 10 | "translatorType": 4, 11 | "browserSupport": "gcsibv", 12 | "lastUpdated": "2024-02-03 14:24:01" 13 | } 14 | 15 | /* 16 | ***** BEGIN LICENSE BLOCK ***** 17 | 18 | Weibo Translator 19 | Copyright © 2020-2021 pixiandouban, 2023 jiaojiaodubai 20 | 21 | This file is part of Zotero. 22 | 23 | Zotero is free software: you can redistribute it and/or modify 24 | it under the terms of the GNU Affero General Public License as published by 25 | the Free Software Foundation, either version 3 of the License, or 26 | (at your option) any later version. 27 | 28 | Zotero is distributed in the hope that it will be useful, 29 | but WITHOUT ANY WARRANTY; without even the implied warranty of 30 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 31 | GNU Affero General Public License for more details. 32 | 33 | You should have received a copy of the GNU Affero General Public License 34 | along with Zotero. If not, see . 35 | 36 | ***** END LICENSE BLOCK ***** 37 | */ 38 | 39 | function detectWeb(doc, url) { 40 | if (/\/\d+\/[a-z\d]+/i.test(url)) { 41 | return 'forumPost'; 42 | } 43 | else if (getSearchResults(doc, true)) { 44 | return 'multiple'; 45 | } 46 | return false; 47 | } 48 | 49 | function select(field) { 50 | let pages = [ 51 | // https://weibo.com/ 52 | // https://weibo.com/7467277921/NBaFziUqv?refer_flag=1001030103_ 53 | { 54 | row: 'article.woo-panel-main', 55 | name: 'a[class*="head_name"]', 56 | time: 'a[class*="head-info_time"]', 57 | detail: 'div[class*="detail_text"]', 58 | button: '[class*="toolbar_main"] > [class*="toolbar_item"]' 59 | }, 60 | // https://s.weibo.com/weibo?q=%E5%A4%A9%E6%B0%94 61 | { 62 | row: '[action-type="feed_list_item"]', 63 | name: 'a.name', 64 | time: '.from > a', 65 | detail: '[node-type*=feed_list_content]', 66 | button: '.card-act li' 67 | } 68 | ]; 69 | return pages.map(page => page[field]).join(','); 70 | } 71 | 72 | function getSearchResults(doc, checkOnly) { 73 | var items = {}; 74 | var found = false; 75 | var rows = doc.querySelectorAll(select('row')); 76 | for (let row of rows) { 77 | let href = attr(row, select('time'), 'href'); 78 | let title = `${text(row, select('name'))}:${text(row, select('detail')).substring(0, 30)}……`; 79 | if (!href || !title) continue; 80 | if (checkOnly) return true; 81 | found = true; 82 | items[href] = title; 83 | } 84 | return found ? items : false; 85 | } 86 | 87 | async function doWeb(doc, url) { 88 | if (detectWeb(doc, url) == 'multiple') { 89 | let rows = Array.from(doc.querySelectorAll(select('row'))); 90 | let items = await Zotero.selectItems(getSearchResults(doc, false)); 91 | if (!items) return; 92 | for (let url of Object.keys(items)) { 93 | let row = rows.find(row => row.querySelector(`a[href="${url}"]`)); 94 | let href = row.querySelector(select('time')).href; 95 | await scrape(row, href); 96 | } 97 | } 98 | else { 99 | await scrape(doc, url); 100 | } 101 | } 102 | 103 | async function scrape(doc, url = doc.location.href) { 104 | var newItem = new Z.Item('forumPost'); 105 | let expand = doc.querySelector('.expand'); 106 | if (expand && expand.textContent.includes('展开')) { 107 | await expand.click(); 108 | let startTime = Date.now(); 109 | // .expand展开后会变为.collapse 110 | while (expand && Date.now() - startTime < 5000) { 111 | expand = doc.querySelector('.expand'); 112 | await new Promise(resolve => setTimeout(resolve, 200)); 113 | } 114 | } 115 | let detail = text(doc, select('detail'), 1) || text(doc, select('detail'), 0); 116 | detail = detail.replace(/\s*收起.?$/, ''); 117 | newItem.title = tryMatch(detail, /【(.+?)】/, 1).replace(/#/g, '') 118 | || tryMatch(detail, /#(.+?)#/, 1) 119 | || `${text(doc, select('name'))}的微博`; 120 | newItem.abstractNote = detail; 121 | newItem.forumTitle = '新浪微博'; 122 | let time = text(doc, select('time')); 123 | let today = new Date(); 124 | Z.debug(time); 125 | if (/(今天|小时|分钟)/.test(time)) { 126 | newItem.date = ZU.strToISO(today.toLocaleDateString()); 127 | } 128 | else if (time.includes('昨天')) { 129 | let yesterday = new Date(); 130 | yesterday.setDate(today.getDate() - 1); 131 | newItem.date = ZU.strToISO(`${yesterday.getFullYear()}-${yesterday.getMonth() + 1}-${yesterday.getDate()}`); 132 | } 133 | // x月x日 134 | // x-x xx:xx 135 | else if (/^\d{2}\D\d{1,2}\D\d{1,2}/.test(time)) { 136 | newItem.date = ZU.strToISO(`${today.getFullYear().toString().substring(0, 2)}${time.replace(/\b(\d)\b/g, '0$1')}`); 137 | } 138 | else { 139 | newItem.date = ZU.strToISO(time); 140 | } 141 | newItem.url = tryMatch(url, /^.+?\/\d+\/[a-z\d]+/i) || url; 142 | newItem.language = 'zh-CN'; 143 | // .card-act li见于关键词搜索 144 | // https://s.weibo.com/weibo?q=%E5%A4%A9%E6%B0%94 145 | extra.add('reship', text(select('button'), 0)); 146 | extra.add('comment', text(select('button'), 1)); 147 | extra.add('like', text(select('button'), 2)); 148 | extra.add('Status', text(doc, '[class*="head-info_edit"]'), true); 149 | newItem.creators.push({ 150 | firstName: '', 151 | lastName: text(doc, select('name')), 152 | creatorType: 'author', 153 | fieldMode: 1 154 | }); 155 | newItem.attachments.push({ 156 | title: 'Snapshot', 157 | document: doc 158 | }); 159 | newItem.extra = extra.toString(); 160 | newItem.complete(); 161 | } 162 | 163 | /** 164 | * Attempts to get the part of the pattern described from the character, 165 | * and returns an empty string if not match. 166 | * @param {String} string 167 | * @param {RegExp} pattern 168 | * @param {Number} index 169 | * @returns 170 | */ 171 | function tryMatch(string, pattern, index = 0) { 172 | if (!string) return ''; 173 | let match = string.match(pattern); 174 | return (match && match[index]) 175 | ? match[index] 176 | : ''; 177 | } 178 | 179 | const extra = { 180 | clsFields: [], 181 | elseFields: [], 182 | add: function (key, value, cls = false) { 183 | if (value && cls) { 184 | this.clsFields.push([key, value]); 185 | } 186 | else if (value) { 187 | this.elseFields.push([key, value]); 188 | } 189 | }, 190 | toString: function (original) { 191 | return original 192 | ? [ 193 | ...this.clsFields.map(entry => `${entry[0]}: ${entry[1]}`), 194 | original.replace(/^\n|\n$/g, ''), 195 | ...this.elseFields.map(entry => `${entry[0]}: ${entry[1]}`) 196 | ].join('\n') 197 | : [...this.clsFields, ...this.elseFields] 198 | .map(entry => `${entry[0]}: ${entry[1]}`) 199 | .join('\n'); 200 | } 201 | }; 202 | 203 | /** BEGIN TEST CASES **/ 204 | var testCases = [ 205 | { 206 | "type": "web", 207 | "url": "https://weibo.com/newlogin?tabtype=weibo&gid=102803&openLoginLayer=0&url=https%3A%2F%2Fweibo.com%2F", 208 | "items": "multiple" 209 | }, 210 | { 211 | "type": "web", 212 | "url": "https://s.weibo.com/weibo?q=%E5%A4%A9%E6%B0%94", 213 | "items": "multiple" 214 | }, 215 | { 216 | "type": "web", 217 | "url": "https://weibo.com/1871802012", 218 | "items": "multiple" 219 | }, 220 | { 221 | "type": "web", 222 | "url": "https://weibo.com/7467277921/NBaFziUqv?refer_flag=1001030103_", 223 | "items": [ 224 | { 225 | "itemType": "forumPost", 226 | "title": "大金砖来尔滨啦!迪拜小哥从沙漠来感受尔滨冬天", 227 | "creators": [ 228 | { 229 | "firstName": "", 230 | "lastName": "西部决策", 231 | "creatorType": "author", 232 | "fieldMode": 1 233 | } 234 | ], 235 | "date": "2024-01-10", 236 | "abstractNote": "【大金砖来尔滨啦!#迪拜小哥从沙漠来感受尔滨冬天# 】#全球媒体争相报道尔滨盛况# #尔滨大火引来迪拜大金砖# 1月9日,黑龙江哈尔滨。为感受哈尔滨的冬天,迪拜小哥特意从沙漠来到魅力四射的哈尔滨并手动点赞表示尔滨很棒。网友纷纷表示,尔滨欢迎国际友人!@西部决策 西部决策的微博视频 ​​​", 237 | "extra": "Status: 已编辑\nreship: 85\ncomment: 194\nlike: 5481", 238 | "forumTitle": "新浪微博", 239 | "language": "zh-CN", 240 | "url": "https://weibo.com/7467277921/NBaFziUqv", 241 | "attachments": [ 242 | { 243 | "title": "Snapshot", 244 | "mimeType": "text/html" 245 | } 246 | ], 247 | "tags": [], 248 | "notes": [], 249 | "seeAlso": [] 250 | } 251 | ] 252 | } 253 | ] 254 | /** END TEST CASES **/ 255 | -------------------------------------------------------------------------------- /Standard Full-text Database - NLC.js: -------------------------------------------------------------------------------- 1 | { 2 | "translatorID": "33ed4133-f48b-45e4-8f00-9b8c22342c0b", 3 | "label": "Standard Full-text Database - NLC", 4 | "creator": "018, jiaojiaodubai", 5 | "target": "https?://vpn2\\.nlc\\.cn/prx", 6 | "minVersion": "3.0", 7 | "maxVersion": "", 8 | "priority": 100, 9 | "inRepository": true, 10 | "translatorType": 4, 11 | "browserSupport": "gcsibv", 12 | "lastUpdated": "2024-01-16 16:33:47" 13 | } 14 | 15 | /* 16 | ***** BEGIN LICENSE BLOCK ***** 17 | 18 | Copyright © 2020 018, 2024 jiaojiaoduabi 19 | 20 | This file is part of Zotero. 21 | 22 | Zotero is free software: you can redistribute it and/or modify 23 | it under the terms of the GNU Affero General Public License as published by 24 | the Free Software Foundation, either version 3 of the License, or 25 | (at your option) any later version. 26 | 27 | Zotero is distributed in the hope that it will be useful, 28 | but WITHOUT ANY WARRANTY; without even the implied warranty of 29 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 30 | GNU Affero General Public License for more details. 31 | 32 | You should have received a copy of the GNU Affero General Public License 33 | along with Zotero. If not, see . 34 | 35 | ***** END LICENSE BLOCK ***** 36 | */ 37 | 38 | function detectWeb(doc, url) { 39 | if (url.includes('/standard/')) { 40 | return 'standard'; 41 | } 42 | else if (getSearchResults(doc, true)) { 43 | return 'multiple'; 44 | } 45 | return false; 46 | } 47 | 48 | function getSearchResults(doc, checkOnly) { 49 | var items = {}; 50 | var found = false; 51 | var rows = doc.querySelectorAll('.titleft'); 52 | for (let row of rows) { 53 | let stdNumber = tryMatch(attr(row, 'a', 'onclick'), ',"(.+)"', 1); 54 | Z.debug(stdNumber); 55 | let title = ZU.trimInternal(`${text(row, 'a', 0)} ${text(row, 'a', 1)}`); 56 | Z.debug(title); 57 | if (!stdNumber || !title) continue; 58 | if (checkOnly) return true; 59 | found = true; 60 | items[stdNumber] = title; 61 | } 62 | return found ? items : false; 63 | } 64 | 65 | async function doWeb(doc, url) { 66 | if (detectWeb(doc, url) == 'multiple') { 67 | let items = await Zotero.selectItems(getSearchResults(doc, false)); 68 | if (!items) return; 69 | for (let stdNumber of Object.keys(items)) { 70 | // browser needed 71 | Z.debug(`${tryMatch(url, /^.+\/view\//)}standard/${encodeURIComponent(stdNumber)}/?`.replace(/(%)(\w{2})/g, '$125$2')); 72 | await scrape(await requestDocument(`${tryMatch(url, /^.+\/view\//)}standard/${encodeURIComponent(stdNumber)}/?`.replace(/(%)(\w{2})/g, '$125$2'))); 73 | } 74 | } 75 | else { 76 | await scrape(doc, url); 77 | } 78 | } 79 | 80 | async function scrape(doc, url = doc.location.href) { 81 | Z.debug(doc.body.innerText); 82 | const labels = new Labels(doc, '.detail_content tr:not([height])'); 83 | var newItem = new Z.Item('standard'); 84 | newItem.title = text(doc, '.detail_title .td_content'); 85 | newItem.extra = ''; 86 | newItem.extra += addExtra('original-title', text(doc, '.detail_title .td_content', 1)); 87 | // newItem.abstractNote = 摘要; 88 | // newItem.organization = 组织; 89 | // newItem.committee = 委员会; 90 | // newItem.type = 类型; 91 | newItem.number = text(doc, '.th_title').replace(/-/g, '—'); 92 | // newItem.versionNumber = 版本; 93 | newItem.status = labels.get('标准状态'); 94 | newItem.date = labels.get('发布日期'); 95 | newItem.url = url; 96 | newItem.numPages = tryMatch(labels.get('页数'), /\d+/); 97 | newItem.language = labels.get('出版语种'); 98 | newItem.libraryCatalog = '国家图书馆标准全文数据库'; 99 | newItem.extra += addExtra('applyDate', labels.get('实施日期')); 100 | newItem.extra += addExtra('ICS', labels.get('标准ICS号')); 101 | newItem.extra += addExtra('CCS', labels.get('中标分类号')); 102 | newItem.extra += addExtra('substitute-for', labels.get('代替标准')); 103 | newItem.extra += addExtra('substitute-by', labels.get('被代替标准')); 104 | newItem.extra += addExtra('reference', labels.get('引用标准')); 105 | newItem.extra += addExtra('adopted', labels.get('采用标准')); 106 | let pdfLink = doc.querySelector('a[href*="downPdf"]'); 107 | if (pdfLink) { 108 | newItem.attachments.push({ 109 | url: pdfLink.href, 110 | title: 'Full Text PDF', 111 | mimeType: 'application/pdf' 112 | }); 113 | } 114 | newItem.complete(); 115 | } 116 | 117 | class Labels { 118 | constructor(doc, selector) { 119 | this.data = []; 120 | this.emptyElm = doc.createElement('div'); 121 | Array.from(doc.querySelectorAll(selector)) 122 | // avoid nesting 123 | .filter(element => !element.querySelector(selector)) 124 | // avoid empty 125 | .filter(element => !/^\s*$/.test(element.textContent)) 126 | .forEach((element) => { 127 | const elmCopy = element.cloneNode(true); 128 | // avoid empty text 129 | while (/^\s*$/.test(elmCopy.firstChild.textContent)) { 130 | // Z.debug(elementCopy.firstChild.textContent); 131 | elmCopy.removeChild(elmCopy.firstChild); 132 | // Z.debug(elementCopy.firstChild.textContent); 133 | } 134 | if (elmCopy.childNodes.length > 1) { 135 | const key = elmCopy.removeChild(elmCopy.firstChild).textContent.replace(/\s/g, ''); 136 | this.data.push([key, elmCopy]); 137 | } 138 | else { 139 | const text = ZU.trimInternal(elmCopy.textContent); 140 | const key = tryMatch(text, /^[[【]?.+?[】\]::]/).replace(/\s/g, ''); 141 | elmCopy.textContent = tryMatch(text, /^[[【]?.+?[】\]::]\s*(.+)/, 1); 142 | this.data.push([key, elmCopy]); 143 | } 144 | }); 145 | } 146 | 147 | get(label, element = false) { 148 | if (Array.isArray(label)) { 149 | const results = label 150 | .map(aLabel => this.get(aLabel, element)); 151 | const keyVal = element 152 | ? results.find(element => !/^\s*$/.test(element.textContent)) 153 | : results.find(string => string); 154 | return keyVal 155 | ? keyVal 156 | : element 157 | ? this.emptyElm 158 | : ''; 159 | } 160 | const pattern = new RegExp(label, 'i'); 161 | const keyVal = this.data.find(arr => pattern.test(arr[0])); 162 | return keyVal 163 | ? element 164 | ? keyVal[1] 165 | : ZU.trimInternal(keyVal[1].textContent) 166 | : element 167 | ? this.emptyElm 168 | : ''; 169 | } 170 | } 171 | 172 | /** 173 | * Attempts to get the part of the pattern described from the character, 174 | * and returns an empty string if not match. 175 | * @param {String} string 176 | * @param {RegExp} pattern 177 | * @param {Number} index 178 | * @returns 179 | */ 180 | function tryMatch(string, pattern, index = 0) { 181 | if (!string) return ''; 182 | let match = string.match(pattern); 183 | return (match && match[index]) 184 | ? match[index] 185 | : ''; 186 | } 187 | 188 | /** 189 | * When value is valid, return a key-value pair in string form. 190 | * @param {String} key 191 | * @param {*} value 192 | * @returns 193 | */ 194 | function addExtra(key, value) { 195 | return value 196 | ? `${key}: ${value}\n` 197 | : ''; 198 | } 199 | 200 | /** BEGIN TEST CASES **/ 201 | var testCases = [ 202 | { 203 | "type": "web", 204 | "url": "https://vpn2.nlc.cn/prx/000/http/192.168.182.80:8983/spcweb/view/standard/GB%252FT%25203793-1983/?", 205 | "items": [ 206 | { 207 | "itemType": "standard", 208 | "title": "检索期刊条目著录规则", 209 | "creators": [], 210 | "date": "1983-07-02", 211 | "extra": "original-title: Descriptive rules for entries of retrieval periodicals\napplyDate: 1984-04-01\nICS: 01.140.20\nCCS: A14", 212 | "libraryCatalog": "国家图书馆标准全文数据库", 213 | "number": "GB/T 3793—1983", 214 | "status": "现行", 215 | "url": "https://vpn2.nlc.cn/prx/000/http/192.168.182.80:8983/spcweb/view/standard/GB%252FT%25203793-1983/?", 216 | "attachments": [ 217 | { 218 | "title": "Full Text PDF", 219 | "mimeType": "application/pdf" 220 | } 221 | ], 222 | "tags": [], 223 | "notes": [], 224 | "seeAlso": [] 225 | } 226 | ] 227 | } 228 | ] 229 | 230 | /** END TEST CASES **/ 231 | -------------------------------------------------------------------------------- /Weixin.js: -------------------------------------------------------------------------------- 1 | { 2 | "translatorID": "5a325508-cb60-42c3-8b0f-d4e3c6441058", 3 | "label": "Weixin", 4 | "creator": "Fushan Wen, jiaojiaodubai", 5 | "target": "^https?://mp\\.weixin\\.qq\\.com", 6 | "minVersion": "3.0", 7 | "maxVersion": "", 8 | "priority": 100, 9 | "inRepository": true, 10 | "translatorType": 4, 11 | "browserSupport": "gcsibv", 12 | "lastUpdated": "2024-02-24 11:31:40" 13 | } 14 | 15 | /* 16 | ***** BEGIN LICENSE BLOCK ***** 17 | 18 | Copyright © 2022 l0o0 19 | 20 | This file is part of Zotero. 21 | 22 | Zotero is free software: you can redistribute it and/or modify 23 | it under the terms of the GNU Affero General Public License as published by 24 | the Free Software Foundation, either version 3 of the License, or 25 | (at your option) any later version. 26 | 27 | Zotero is distributed in the hope that it will be useful, 28 | but WITHOUT ANY WARRANTY; without even the implied warranty of 29 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 30 | GNU Affero General Public License for more details. 31 | 32 | You should have received a copy of the GNU Affero General Public License 33 | along with Zotero. If not, see . 34 | 35 | ***** END LICENSE BLOCK ***** 36 | */ 37 | 38 | 39 | function detectWeb(doc, _url) { 40 | if (attr(doc, 'meta[property="og:type"]', 'content') === 'article') { 41 | return "blogPost"; 42 | } 43 | return false; 44 | } 45 | 46 | async function doWeb(doc, url) { 47 | const newItem = new Z.Item('blogPost'); 48 | const metas = new Map(); 49 | const nodeList = doc.head.querySelectorAll(':scope meta[property^="og:"]'); 50 | for (const node of nodeList) { 51 | metas.set(node.getAttribute('property'), node.content); 52 | } 53 | Z.debug(metas); 54 | newItem.title = metas.get('og:title'); 55 | newItem.abstractNote = metas.get('og:description'); 56 | newItem.blogTitle = text(doc, '#profileBt > a'); 57 | // newItem.websiteType = metas.get('og:site_name'); 58 | newItem.websiteType = '微信公众号'; 59 | newItem.date = text(doc, '#publish_time'); 60 | newItem.url = metas.get('og:url'); 61 | [...new Set([text(doc, '#js_name'), metas.get('og:article:author')])].forEach((creator) => { 62 | creator = ZU.cleanAuthor(creator, 'author'); 63 | if (/[\u4e00-\u9fff]/.test(creator.lastName)) { 64 | creator.lastName = creator.firstName + creator.lastName; 65 | creator.firstName = ''; 66 | creator.fieldMode = 1; 67 | } 68 | newItem.creators.push(creator); 69 | }); 70 | 71 | /* 删除这行启用note记录全文 72 | let note = doc.body.querySelector("#js_content"); 73 | if (note) { 74 | note = `

${newItem.title}

` 75 | + note.innerHTML 76 | .trim() 77 | .replace(/\"/g, "'") 78 | .replace(//g, ""); 79 | newItem.notes.push(note); 80 | } 81 | 删除这行启用note记录全文 */ 82 | newItem.attachments.push({ 83 | url: url, 84 | title: 'Snapshot', 85 | document: doc 86 | }); 87 | newItem.complete(); 88 | } 89 | 90 | /** BEGIN TEST CASES **/ 91 | var testCases = [ 92 | { 93 | "type": "web", 94 | "url": "https://mp.weixin.qq.com/s/NYENhzx7kF7OX_d4DTD4fA", 95 | "items": [ 96 | { 97 | "itemType": "blogPost", 98 | "title": "Zotero 官方安卓测试版来了", 99 | "creators": [ 100 | { 101 | "firstName": "", 102 | "lastName": "学术废物收容所", 103 | "creatorType": "author", 104 | "fieldMode": 1 105 | }, 106 | { 107 | "firstName": "", 108 | "lastName": "l0o0", 109 | "creatorType": "author" 110 | } 111 | ], 112 | "date": "2023-12-26 11:57", 113 | "abstractNote": "来自官方的圣诞礼物🎁——Zotero 安卓测试版来了", 114 | "blogTitle": "学术废物收容所", 115 | "url": "http://mp.weixin.qq.com/s?__biz=MzkyNjUxNjgxNg==&mid=2247483816&idx=1&sn=86afcacc6b0403049380d86e6618cae7&chksm=c2375297f540db817078c81cefd63069b0da43222c8f5b9dfb4ac15eae87f42f0b4555e89fe1#rd", 116 | "websiteType": "微信公众号", 117 | "attachments": [ 118 | { 119 | "title": "Snapshot", 120 | "mimeType": "text/html" 121 | } 122 | ], 123 | "tags": [], 124 | "notes": [], 125 | "seeAlso": [] 126 | } 127 | ] 128 | } 129 | ] 130 | /** END TEST CASES **/ 131 | -------------------------------------------------------------------------------- /Xinhuanet.js: -------------------------------------------------------------------------------- 1 | { 2 | "translatorID": "a038ca9d-9f46-412b-82e5-e58d9f15e026", 3 | "label": "Xinhuanet", 4 | "creator": "jiaojiaodubai", 5 | "target": "^http://www(\\.news\\.cn|\\.xinhuanet\\.com)", 6 | "minVersion": "5.0", 7 | "maxVersion": "", 8 | "priority": 100, 9 | "inRepository": true, 10 | "translatorType": 4, 11 | "browserSupport": "gcsibv", 12 | "lastUpdated": "2024-01-16 10:37:11" 13 | } 14 | 15 | /* 16 | ***** BEGIN LICENSE BLOCK ***** 17 | 18 | Copyright © 2024 jiaojiaodubai 19 | 20 | This file is part of Zotero. 21 | 22 | Zotero is free software: you can redistribute it and/or modify 23 | it under the terms of the GNU Affero General Public License as published by 24 | the Free Software Foundation, either version 3 of the License, or 25 | (at your option) any later version. 26 | 27 | Zotero is distributed in the hope that it will be useful, 28 | but WITHOUT ANY WARRANTY; without even the implied warranty of 29 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 30 | GNU Affero General Public License for more details. 31 | 32 | You should have received a copy of the GNU Affero General Public License 33 | along with Zotero. If not, see . 34 | 35 | ***** END LICENSE BLOCK ***** 36 | */ 37 | 38 | 39 | function detectWeb(doc, url) { 40 | if (/\/\d{8}\//.test(url)) { 41 | return 'webpage'; 42 | } 43 | else if (getSearchResults(doc, true)) { 44 | return 'multiple'; 45 | } 46 | return false; 47 | } 48 | 49 | function getSearchResults(doc, checkOnly) { 50 | var items = {}; 51 | var found = false; 52 | var rows = Array.from(doc.querySelectorAll('a')).filter(element => /\/\d{8}\//.test(element.href)); 53 | for (let row of rows) { 54 | let href = row.href; 55 | let title = ZU.trimInternal(row.textContent); 56 | if (!href || !title) continue; 57 | if (checkOnly) return true; 58 | found = true; 59 | items[href] = title; 60 | } 61 | return found ? items : false; 62 | } 63 | 64 | async function doWeb(doc, url) { 65 | if (detectWeb(doc, url) == 'multiple') { 66 | let items = await Zotero.selectItems(getSearchResults(doc, false)); 67 | if (!items) return; 68 | for (let url of Object.keys(items)) { 69 | await scrape(await requestDocument(url)); 70 | } 71 | } 72 | else { 73 | await scrape(doc, url); 74 | } 75 | } 76 | 77 | async function scrape(doc, url = doc.location.href) { 78 | var newItem = new Z.Item('webpage'); 79 | newItem.title = text(doc, '.head-line > h1'); 80 | newItem.websiteTitle = '新华网'; 81 | newItem.date = `${text(doc, '.header-time > .year')}-${text(doc, '.header-time > .day').replace('/', '-')}`; 82 | newItem.url = url; 83 | newItem.language = 'zh-CN'; 84 | newItem.attachments.push({ 85 | title: 'Snapshot', 86 | document: doc 87 | }); 88 | let authors = Array.from(doc.querySelectorAll('#detailContent > p')) 89 | .map(element => element.innerText) 90 | .filter(element => element.startsWith('  ') || element.startsWith('  ')); 91 | Z.debug(authors); 92 | // “记者”在第二行:http://www.news.cn/politics/20240106/5518a024d70c46e19448ca4c40a926e2/c.html 93 | // “记者”在第二行,顿号分隔:http://www.news.cn/local/20240107/9c08d1cdc9a542adb5014763b800c437/c.html 94 | // “记者”在最后一行,空格分隔:http://www.news.cn/fortune/20240108/10abbd286d9640dfb31a3d1ab1d40603/c.html 95 | // “记者”前有图:http://www.news.cn/politics/20240107/d23e8e94734643e0bea2be961ef90895/c.html 96 | // 避免以“记者”为段首:http://www.xinhuanet.com/20240108/b9bf79b4c46e4676907320d10373b37b/c.html 97 | authors = [authors[0], authors[1], authors[authors.length - 1]].find(string => /\S+记者(?::|\s*)(.+)[))]*/.test(string)) || ''; 98 | Z.debug(authors); 99 | authors = tryMatch( 100 | authors, 101 | /记者(?::|\s*)(.+)[))]*/, 102 | 1 103 | ); 104 | // let editor = text(doc, '.editor'); 105 | // Z.debug(editor); 106 | newItem.creators = [...processName(authors, 'author')]; 107 | newItem.complete(); 108 | } 109 | 110 | function tryMatch(string, pattern, index = 0) { 111 | if (!string) return ''; 112 | let match = string.match(pattern); 113 | return (match && match[index]) 114 | ? match[index] 115 | : ''; 116 | } 117 | 118 | function processName(creators, creatorType) { 119 | creators = creators 120 | .trim() 121 | .replace(/^[(([【[]|[))]】\]]$/g, '') 122 | .replace(/([^\u4e00-\u9fff][\u4e00-\u9fff])\s+([\u4e00-\u9fff](?:[^\u4e00-\u9fff]|$))/g, '$1$2') 123 | .split(/\s+|、|:|:/) 124 | .filter(creator => !/编|(?:记者)|摄|图|文/.test(creator)); 125 | creators = creators.map((creator) => { 126 | creator = ZU.cleanAuthor(creator, creatorType); 127 | creator.fieldMode = 1; 128 | return creator; 129 | }); 130 | return creators; 131 | } 132 | 133 | /** BEGIN TEST CASES **/ 134 | var testCases = [ 135 | { 136 | "type": "web", 137 | "url": "http://www.news.cn/politics/20240106/5518a024d70c46e19448ca4c40a926e2/c.html", 138 | "items": [ 139 | { 140 | "itemType": "webpage", 141 | "title": "以高质量监督服务高质量发展——坚定不移推进全面从严治党之“监督篇”", 142 | "creators": [ 143 | { 144 | "firstName": "", 145 | "lastName": "刘硕", 146 | "creatorType": "author", 147 | "fieldMode": 1 148 | } 149 | ], 150 | "date": "2024-01-06", 151 | "language": "zh-CN", 152 | "url": "http://www.news.cn/politics/20240106/5518a024d70c46e19448ca4c40a926e2/c.html", 153 | "websiteTitle": "新华网", 154 | "attachments": [ 155 | { 156 | "title": "Snapshot", 157 | "mimeType": "text/html" 158 | } 159 | ], 160 | "tags": [], 161 | "notes": [], 162 | "seeAlso": [] 163 | } 164 | ] 165 | }, 166 | { 167 | "type": "web", 168 | "url": "http://www.news.cn/fortune/20240108/10abbd286d9640dfb31a3d1ab1d40603/c.html", 169 | "items": [ 170 | { 171 | "itemType": "webpage", 172 | "title": "政策持续完善 银发经济再拓空间", 173 | "creators": [ 174 | { 175 | "firstName": "", 176 | "lastName": "张莫", 177 | "creatorType": "author", 178 | "fieldMode": 1 179 | }, 180 | { 181 | "firstName": "", 182 | "lastName": "陈涵旸", 183 | "creatorType": "author", 184 | "fieldMode": 1 185 | } 186 | ], 187 | "date": "2024-01-08", 188 | "language": "zh-CN", 189 | "url": "http://www.news.cn/fortune/20240108/10abbd286d9640dfb31a3d1ab1d40603/c.html", 190 | "websiteTitle": "新华网", 191 | "attachments": [ 192 | { 193 | "title": "Snapshot", 194 | "mimeType": "text/html" 195 | } 196 | ], 197 | "tags": [], 198 | "notes": [], 199 | "seeAlso": [] 200 | } 201 | ] 202 | }, 203 | { 204 | "type": "web", 205 | "url": "http://www.news.cn/local/20240107/9c08d1cdc9a542adb5014763b800c437/c.html", 206 | "items": [ 207 | { 208 | "itemType": "webpage", 209 | "title": "新华鲜报丨“尔滨”引发南北文旅拉歌:掏出“家底”请你来", 210 | "creators": [ 211 | { 212 | "firstName": "", 213 | "lastName": "吉哲鹏", 214 | "creatorType": "author", 215 | "fieldMode": 1 216 | }, 217 | { 218 | "firstName": "", 219 | "lastName": "孙敏", 220 | "creatorType": "author", 221 | "fieldMode": 1 222 | }, 223 | { 224 | "firstName": "", 225 | "lastName": "严勇", 226 | "creatorType": "author", 227 | "fieldMode": 1 228 | } 229 | ], 230 | "date": "2024-01-07", 231 | "language": "zh-CN", 232 | "url": "http://www.news.cn/local/20240107/9c08d1cdc9a542adb5014763b800c437/c.html", 233 | "websiteTitle": "新华网", 234 | "attachments": [ 235 | { 236 | "title": "Snapshot", 237 | "mimeType": "text/html" 238 | } 239 | ], 240 | "tags": [], 241 | "notes": [], 242 | "seeAlso": [] 243 | } 244 | ] 245 | }, 246 | { 247 | "type": "web", 248 | "url": "http://www.news.cn/politics/20240107/d23e8e94734643e0bea2be961ef90895/c.html", 249 | "items": [ 250 | { 251 | "itemType": "webpage", 252 | "title": "第40次南极考察丨中国在极地布放首个生态潜标", 253 | "creators": [ 254 | { 255 | "firstName": "", 256 | "lastName": "周圆", 257 | "creatorType": "author", 258 | "fieldMode": 1 259 | } 260 | ], 261 | "date": "2024-01-07", 262 | "language": "zh-CN", 263 | "url": "http://www.news.cn/politics/20240107/d23e8e94734643e0bea2be961ef90895/c.html", 264 | "websiteTitle": "新华网", 265 | "attachments": [ 266 | { 267 | "title": "Snapshot", 268 | "mimeType": "text/html" 269 | } 270 | ], 271 | "tags": [], 272 | "notes": [], 273 | "seeAlso": [] 274 | } 275 | ] 276 | }, 277 | { 278 | "type": "web", 279 | "url": "http://www.xinhuanet.com/", 280 | "items": "multiple" 281 | } 282 | ] 283 | /** END TEST CASES **/ 284 | -------------------------------------------------------------------------------- /data/data.json: -------------------------------------------------------------------------------- 1 | { 2 | "Angle": "元照出版", 3 | "Baidu Baike": "百度百科", 4 | "Baidu Scholar": "百度学术", 5 | "Belt and Road Database": "“一带一路”数据库", 6 | "BiliBili": "Bilibili视频", 7 | "CCPINFO": "国家出版发行信息公共服务平台", 8 | "chaoxingqikan": "超星期刊", 9 | "China Judgements Online": "中国裁判文书网", 10 | "China Social Science Library": "中国社会科学文库", 11 | "ChinaXiv": "中国科学院科技论文预发布平台", 12 | "CHINESE JOURNAL OF LAW": "法学研究", 13 | "CNBKSY": "全国报刊索引", 14 | "CNKI": "中国知网", 15 | "CNKI CHKD": "中国医院知识总库", 16 | "CNKI e-Books": "中国知网电子书", 17 | "CNKI Gongjushu": "中国知网工具书总库", 18 | "CNKI Industry": "中国知网行业知识服务平台", 19 | "CNKI Law": "中国法律知识资源总库", 20 | "CNKI Refer": "中国知网Endote导出格式", 21 | "CNKI RefWorks": "中国知网RefWorks导出格式", 22 | "CNKI Scholar": "中国知网外文总库", 23 | "CNKI thinker": "中国知网心可图书馆", 24 | "CNKI TIKS": "中国知网科技创新知识服务平台", 25 | "CNSDA": "中国学术调查数据资料库", 26 | "CQVIP": "维普网", 27 | "CQVIP Knowledge": "维普经纶知识服务平台", 28 | "CQVIP Qikan": "维普期刊", 29 | "Cubox": "Cubox", 30 | "Dangdang": "当当图书", 31 | "doc.taixueshu": " 钛学术文献服务平台", 32 | "Douban": "豆瓣", 33 | "dpaper": "中国科学院文献情报中心", 34 | "Duxiu": "读秀", 35 | "E-Tiller": "勤云科技", 36 | "Encyclopedia of China 3rd": "中国大百科全书", 37 | "epaper.gmw.cn": "光明网(电子版)", 38 | "flk.npc.gov.cn": "国家法律法规数据库", 39 | "Founder": "方正鸿云", 40 | "GFSOSO": "谷粉搜搜", 41 | "gov.cn Policy": "国务院政策文件库", 42 | "incoPat": "合享", 43 | "Jd": "京东", 44 | "Jikan Full Text Database": "集刊全文数据库", 45 | "Lawbank": "法源法律網", 46 | "MagTech": "玛格泰克", 47 | "Modern History": "抗日战争与近代中日关系文献数据平台", 48 | "National Public Service Platform for Standards Information - China": "全国标准信息公共服务平台", 49 | "National Science and Technology Library - China": "国家科技图书文献中心", 50 | "National Science and Technology Report Service - China": "国家科技报告服务系统", 51 | "National Standards Open System - China": "国家标准全文公开系统", 52 | "Ncpssd": "国家哲学社会科学文献中心", 53 | "NDLTD": "臺灣博碩士論文知識加值系統", 54 | "nlc.cn": "国图-中国标准在线服务网", 55 | "NTU Digital Library of Buddhist Studies": "臺大佛學數位圖書館", 56 | "PatentStar": "专利之星", 57 | "People's Daily Database": "人民日报图文数据库", 58 | "People's Daily Epaper": "人民日报(电子版)", 59 | "People's Daily Online": "人民网", 60 | "Pishu Database": "皮书数据库", 61 | "PKULaw": "北大法宝", 62 | "pm.tsgyun": "泉方本地PubMed", 63 | "ProQuestCN Thesis": "ProQuest学位论文(中国)", 64 | "Pss-System": "国家知识产权局专利检索及分析系统", 65 | "Publications Data Center - China": "国家版本馆版权数据中心", 66 | "PubScholar": "PubScholar公益学术平台", 67 | "QStheory": "求是网", 68 | "RDFYBK": "人大复印报刊", 69 | "RHHZ": "仁和汇智", 70 | "Rural Studies Database": "乡村研究数据库", 71 | "sanmin.com.tw": "三民網路書店", 72 | "Science Reading": "科学文库", 73 | "sharing.com.tw": "新學林網路書店", 74 | "Sina Weibo": "新浪微博", 75 | "SKCTK": "中国社会科学词条库", 76 | "Soopat": "Soopat专利", 77 | "Spc.org.cn": "中国标准在线服务网", 78 | "Standard Full-text Database - NLC": "国家图书馆标准全文数据库", 79 | "stats.gov.cn": "国家统计局", 80 | "SuperLib": "全国图书馆联盟", 81 | "TOAJ": "臺灣學術期刊開放取用平台", 82 | "Wanfang Data": "万方", 83 | "Wanfang Med": "万方医疗", 84 | "Weixin": "微信", 85 | "Wenjin": "国图-文津", 86 | "xiaoyuzhouFM": "小宇宙FM", 87 | "Xinhuanet": "新华网", 88 | "Yiigle": "中华医学期刊全文数据库", 89 | "zhangqiaokeyan": " 掌桥科研", 90 | "Zhihu": "知乎", 91 | "Zhihuiya": "智慧芽", 92 | "Zhizhen": "超星发现" 93 | } -------------------------------------------------------------------------------- /data/updateJSON.js: -------------------------------------------------------------------------------- 1 | (async () => { 2 | const { repo, parsed } = require('../.ci/eslint-plugin-zotero-translator/processor').support; 3 | const { exec } = require('child_process'); 4 | const path = require('path'); 5 | const fs = require('fs'); 6 | 7 | const excepts = ['BibTeX.js', 'RefWorks Tagged.js']; 8 | 9 | async function getRecentCommits(filepath, count = 3) { 10 | const cmd = `git log -n ${count} --pretty=format:"{\\"author\\": \\"%an\\", \\"date\\": \\"%ci\\", \\"message\\": \\"%s\\"}" -- "${filepath}"`; 11 | return new Promise((resolve, reject) => { 12 | exec(cmd, (error, stdout, _stderr) => { 13 | if (error) { 14 | reject(error); 15 | } 16 | else { 17 | resolve(stdout.trim() 18 | .split('\n') 19 | .filter(line => line) 20 | .map(line => JSON.parse(line))); 21 | } 22 | }); 23 | }); 24 | } 25 | 26 | const labelMap = JSON.parse(fs.readFileSync(`${repo}/data/data.json`, 'utf-8')); 27 | const updateTimeMap = { }; 28 | let translators = { }; 29 | 30 | try { 31 | translators = JSON.parse(fs.readFileSync(`${repo}/data/dashboard.json`, 'utf-8')); 32 | } 33 | catch (error) { 34 | console.log(error); 35 | } 36 | 37 | for (const basename of fs.readdirSync(repo).sort()) { 38 | if (!basename.endsWith('.js') || excepts.includes(basename)) continue; 39 | const fullpath = path.join(repo, basename); 40 | const parsedFile = parsed(fullpath); 41 | if (!translators[basename]) { 42 | translators[basename] = { }; 43 | } 44 | const translator = translators[basename]; 45 | const enLabel = parsedFile.header.fields.label; 46 | const testCasesText = parsedFile.testcases.text; 47 | try { 48 | translator.header = parsedFile.header.fields; 49 | translator.testCases = testCasesText ? JSON.parse(testCasesText) : []; 50 | translator.zhLabel = labelMap[enLabel] || enLabel; 51 | await getRecentCommits(fullpath) 52 | .then(commits => translator.trends = commits) 53 | .catch(err => console.error('Error occurred:', err)); 54 | updateTimeMap[basename] = { 55 | label: translator.zhLabel, 56 | lastUpdated: translator.header.lastUpdated 57 | }; 58 | } 59 | catch (error) { 60 | console.log(basename); 61 | console.log(error); 62 | } 63 | } 64 | 65 | // console.log(translators); 66 | 67 | fs.writeFileSync( 68 | `${repo}/data/data.json`, 69 | JSON.stringify( 70 | Object.fromEntries(Object.entries(labelMap).sort((a, b) => a[0].localeCompare(b[0]))), 71 | null, 72 | 2 73 | ) 74 | ); 75 | fs.writeFileSync(`${repo}/data/translators.json`, JSON.stringify(updateTimeMap, null, 2)); 76 | fs.writeFileSync(`${repo}/data/dashboard.json`, JSON.stringify(translators, null, 2)); 77 | })(); 78 | -------------------------------------------------------------------------------- /deleted.txt: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/l0o0/translators_CN/209839d6cac76925df8c28e203ef07358eed2d03/deleted.txt -------------------------------------------------------------------------------- /dpaper.js: -------------------------------------------------------------------------------- 1 | { 2 | "translatorID": "78ba8722-6748-47f4-9976-8985d75a220c", 3 | "label": "dpaper", 4 | "creator": "with, jiaojiaodubai", 5 | "target": "http://dpaper\\.las\\.ac\\.cn", 6 | "minVersion": "5.0", 7 | "maxVersion": "", 8 | "priority": 100, 9 | "inRepository": true, 10 | "translatorType": 4, 11 | "browserSupport": "gcsibv", 12 | "lastUpdated": "2024-06-20 16:34:51" 13 | } 14 | 15 | /* 16 | ***** BEGIN LICENSE BLOCK ***** 17 | 18 | Copyright © 2021 with9, 2023 jiaojiaodubai 19 | 20 | This file is part of Zotero. 21 | 22 | Zotero is free software: you can redistribute it and/or modify 23 | it under the terms of the GNU Affero General Public License as published by 24 | the Free Software Foundation, either version 3 of the License, or 25 | (at your option) any later version. 26 | 27 | Zotero is distributed in the hope that it will be useful, 28 | but WITHOUT ANY WARRANTY; without even the implied warranty of 29 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 30 | GNU Affero General Public License for more details. 31 | 32 | You should have received a copy of the GNU Affero General Public License 33 | along with Zotero. If not, see . 34 | 35 | ***** END LICENSE BLOCK ***** 36 | */ 37 | 38 | function detectWeb(doc) { 39 | if (text(doc, '#education')) { 40 | return "thesis"; 41 | } 42 | return false; 43 | } 44 | 45 | async function doWeb(doc, url) { 46 | await scrape(doc, url); 47 | } 48 | 49 | async function scrape(doc, url) { 50 | var newItem = new Z.Item('thesis'); 51 | newItem.title = text(doc, '#title_cn'); 52 | if (text(doc, '#more_abstract_cn') === "【显示更多内容】") { 53 | //模拟点击显示更多摘要 54 | await doc.getElementById('more_abstract_cn').click(); 55 | } 56 | newItem.abstractNote = text(doc, '#abstract_cn'); 57 | newItem.creators.push(ZU.cleanAuthor(text(doc, '#author_name > a'), 'author')); 58 | let tutor = doc.querySelectorAll('#teacher_name > a'); 59 | tutor.forEach(creator => newItem.creators.push(ZU.cleanAuthor(creator.textContent, 'contributor'))); 60 | newItem.creators.forEach((creator) => { 61 | if (/[\u4e00-\u9fa5]/.test(creator.lastName)) { 62 | creator.fieldMode = 1; 63 | } 64 | }); 65 | newItem.tags.push(text(doc, '#keyword_cn').split(/[,;,;]/g)); 66 | //毕业时间作为论文发表时间 67 | newItem.date = ZU.strToISO(text(doc, '#education_grant_time')); 68 | newItem.thesisType = `${text(doc, '#education')}学位论文`; 69 | newItem.university = text(doc, '#grant_institution'); 70 | newItem.url = tryMatch(url, /^.+paperID=\w+/i); 71 | extra.add('original-title', text(doc, '#title_other'), true); 72 | extra.add('major', text(doc, 'major')); 73 | extra.add('CSTR', text(doc, '#cstr')); 74 | newItem.extra = extra.toString(); 75 | newItem.complete(); 76 | } 77 | 78 | 79 | /** 80 | * Attempts to get the part of the pattern described from the character, 81 | * and returns an empty string if not match. 82 | * @param {String} string 83 | * @param {RegExp} pattern 84 | * @param {Number} index 85 | * @returns 86 | */ 87 | function tryMatch(string, pattern, index = 0) { 88 | if (!string) return ''; 89 | let match = string.match(pattern); 90 | return (match && match[index]) 91 | ? match[index] 92 | : ''; 93 | } 94 | 95 | const extra = { 96 | clsFields: [], 97 | elseFields: [], 98 | add: function (key, value, cls = false) { 99 | if (value && cls) { 100 | this.clsFields.push([key, value]); 101 | } 102 | else if (value) { 103 | this.elseFields.push([key, value]); 104 | } 105 | }, 106 | toString: function () { 107 | return [...this.clsFields, ...this.elseFields] 108 | .map(entry => `${entry[0]}: ${entry[1]}`) 109 | .join('\n'); 110 | } 111 | };/** BEGIN TEST CASES **/ 112 | var testCases = [ 113 | { 114 | "type": "web", 115 | "url": "http://dpaper.las.ac.cn/Dpaper/detail/detailNew?paperID=20172916&title=%E7%BC%85%E7%94%B8%E5%8C%97%E9%83%A8%E5%9B%A0%E9%81%93%E6%94%AF%E6%B9%96%E9%B1%BC%E7%B1%BB%E7%89%A9%E7%A7%8D%E5%A4%9A%E6%A0%B7%E6%80%A7&author=THINN%20SU%20TIN&highsearch=training_institution_all%253A345345345%25E5%258A%25A8%25E7%2589%25A9345345345%2520%2520234234234234234234%2520%2520specification_training_institution_str%253A%25E4%25B8%25AD%25E5%259B%25BD%25E7%25A7%2591%25E5%25AD%25A6%25E9%2599%25A2%25E6%2598%2586%25E6%2598%258E%25E5%258A%25A8%25E7%2589%25A9%25E7%25A0%2594%25E7%25A9%25B6%25E6%2589%2580&sortField=score%2520desc%252Cid&start=0&actionType=Browse&searchText=%E5%8A%A8%E7%89%A9", 116 | "items": [ 117 | { 118 | "itemType": "thesis", 119 | "title": "缅甸北部因道支湖鱼类物种多样性", 120 | "creators": [ 121 | { 122 | "firstName": "THINN SU", 123 | "lastName": "TIN", 124 | "creatorType": "author" 125 | }, 126 | { 127 | "firstName": "", 128 | "lastName": "陈小勇", 129 | "creatorType": "contributor", 130 | "fieldMode": 1 131 | } 132 | ], 133 | "date": "2020-07-01", 134 | "abstractNote": "因道支湖是缅甸最大的淡水湖之一,位于缅甸北部密支那地区,在北纬25°5'–25°20'和东经96°18'–96°23'之间,海拔约550英尺。在雨季过后,由于湖水蔓延到周围的城市,使得湖泊的面积变得更大。因道支湖具有丰富的水生生态系统和生物多样性。并且因道支湖具有特有种和稀有种鱼类。因此本文的主要目的是保护因道支湖特有种和稀有种鱼类,使鱼类资源得到可持续性发展,以及为相关的保护措施提一些建议。本文以缅甸北部的因道支湖为研究区域,借助香农–威纳指数计算物种多样性指数、运用PAST 4.02软件包采用主成成分分析法(PCA)对鱼类栖息地进行分析、采用SPSS软件包和IUCN红色名录标准对鱼类的生态状况进行评价,于2018年10月至2019年10月期间进行采样,对因道支湖的鱼类物种组成、季节性发生和丰富程度进行评估。研究期间共获得31科106种鱼类。在夏季的第一次采样,获得鱼类标本372尾,得到49种鱼类物种。在5月的雨季进行采样,获得鱼类标本444尾,得到74种鱼类物种。包括2018年10月冬季的采样,共获得鱼类标本849尾,得到鱼类物种80种。2019年10月再次进行采样,获得鱼类标本892尾,得到鱼类物种80种。鱼类物种组成以鲤形目居多。在冬季和雨季得到较高的物种数,旱季的物种数偏低。一共获得鱼类物种106种,其中11种为特有种,83种为土著种,并且似鳞头鳅属的Lepidocephalichthys goalparensis为新纪录。鱼类的形态鉴定根据Munro(1955)、Day(1969)、Jayaram(1987)和Talwar & Jhingaran(1991)。此外,根据对湖泊鱼类的调查,我们应该主要保护特有种鱼类,并且制湖泊的捕捞面积,使鱼类资源可持续性发展。同时加强对湖泊保护的科学研究。", 135 | "extra": "original-title: Species Diversity of Fishes in Indawgyi Lake, Northern Myanmar\nCSTR: CSTR:35001.37.02.33170.20200008", 136 | "libraryCatalog": "dpaper", 137 | "thesisType": "硕士学位论文", 138 | "university": "中国科学院大学", 139 | "url": "http://dpaper.las.ac.cn/Dpaper/detail/detailNew?paperID=20172916", 140 | "attachments": [], 141 | "tags": [], 142 | "notes": [], 143 | "seeAlso": [] 144 | } 145 | ] 146 | }, 147 | { 148 | "type": "web", 149 | "url": "http://dpaper.las.ac.cn/Dpaper/detail/detailNew?paperID=20173144&title=%E4%B8%8D%E5%90%8C%E6%A3%AE%E6%9E%97%E7%94%9F%E6%80%81%E7%B3%BB%E7%BB%9F%E8%9A%9C%E8%99%AB%E4%B8%8E%E8%9A%82%E8%9A%81%E5%85%B1%E6%A0%96%E5%85%B3%E7%B3%BB%E7%A0%94%E7%A9%B6&author=%E9%BE%99%E6%B5%B7&highsearch=training_institution_all%253A345345345%25E5%258A%25A8%25E7%2589%25A9345345345&sortField=score%2520desc%252Cid&start=0&actionType=Browse&searchText=%E5%8A%A8%E7%89%A9", 150 | "items": [ 151 | { 152 | "itemType": "thesis", 153 | "title": "不同森林生态系统蚜虫与蚂蚁共栖关系研究", 154 | "creators": [ 155 | { 156 | "firstName": "", 157 | "lastName": "龙海", 158 | "creatorType": "author", 159 | "fieldMode": 1 160 | }, 161 | { 162 | "firstName": "", 163 | "lastName": "乔格侠", 164 | "creatorType": "contributor", 165 | "fieldMode": 1 166 | } 167 | ], 168 | "date": "2020-07-01", 169 | "abstractNote": "种间关系是促进生物多样性形成的动力因素之一,一直被生物学家广泛研究。近些年来,非紧密联系的种间关系已经成为生物学研究的一个新热点,特别是以蚜虫-蚂蚁为典型的非紧密联系的种间关系。全世界蚜虫大约5000种,几乎分布在温暖的北半球,全世界蚂蚁大约有13000种,广布全球。无论从进化历史还是从物种数目上来看,蚜虫蚂蚁都是较为成功的物种。(Strathdee, Howling et al. 1995, Hulle, Pannetier et al. 2003)蜜露是半翅目昆虫取食后产生的含糖物质,这些排泄物主要由蚂蚁收集,但是也有其他昆虫参与。在蚜虫大发生时,如果蜜露不及时清理会导致霉污病的发生,这不仅会影响到植物的生长,也会影响到蚜虫的生长。以蜜露为结点,蚜虫蚂蚁发生复杂的共栖关系。对于种共栖关系的研究已经持续了100多年,其中提出来10多种假说来诠释蚜虫蚂蚁为什么建立共栖关系。但是随着研究的深入,对蚂蚁蚜虫共栖关系的建立有了新的看法。但是大多数的研究都是以生物因素来讨论蚜虫蚂蚁共栖关系,却很少有研究从气候角度来探究蚜虫蚂蚁共栖关系,因此我们选取不同的生态系统,不同的季节来研究蚜虫蚂蚁与气候的关系。对北京东灵山、小龙门森林公园、百花山和广西十万大山地区进行蚜虫蚂蚁共栖物种数据的采集,通过不同季节,不同年份的处理分析发现,与蚜虫建立共栖关系的蚂蚁亚科主要以臭蚁亚科,蚁亚科,切叶蚁亚科为主,其余亚科零星出现。对于东灵山地区来说,在春季与夏季蚜虫物种的优势种群大致相同,但是在秋季由于气候、植被等因素的影响,其优势种群发生改变。对于蚂蚁来说,其优势种群在2018与2019三个季节中并未改变,都为日本弓背蚁,但是其种群数量随着季节的变化不断的改变。万玉斑蚜只与亮毛蚁建立共栖关系。虽然季节对种群数量与物种组成有影响,但是对于整个共栖网络影响较小,不同季节网络参数有一定的波动,但是整体来说较为稳定。 对于在十万大山地区来说,该地区主要以蚜亚科为主。对于蚂蚁来说, 2013年10月、2015年7月和2018年7,2019年7月主要以切叶蚁亚科亚科为主,但在2018年10月则是以蚁亚科和臭蚁亚科为主,季节的波动会影响共栖蚂蚁亚科水平上的组成。虽然季节与年份对种群数量与物种组成有影响,对于整个共栖网络影响较小,不同季节网络参数有一定的波动,但是整体来说较为稳定。对比东灵山来看,十万大山地区网路共栖强度较弱,可能与纬度有一定联系。从蚜虫—蚂蚁共栖关系建立来说,在相同的地区一旦蚜虫与蚂蚁建立共栖关系后,这种关系会一直维持,不会因为季节的变化而导致共栖关系的断裂。从物种组成上来说,与同种蚜虫建立共栖关系的蚂蚁物种会跟随季节,年份的变化而变化,共栖强度也会跟随季节的变化而变化。从共栖网络上来说,共栖网络专化性与连接强度受到季节年份影响的波动较小。系统发育对物种选择建立共栖关系的伙伴与建立共栖关系的次数无影响。蚜虫蚂蚁共栖关系处于动态平衡状态。", 170 | "extra": "original-title: Study on the symbiotic relationship between aphid and ant in different forest ecosystems\nCSTR: CSTR:35001.37.02.33146.20200040", 171 | "libraryCatalog": "dpaper", 172 | "thesisType": "硕士学位论文", 173 | "university": "中国科学院大学", 174 | "url": "http://dpaper.las.ac.cn/Dpaper/detail/detailNew?paperID=20173144", 175 | "attachments": [], 176 | "tags": [], 177 | "notes": [], 178 | "seeAlso": [] 179 | } 180 | ] 181 | } 182 | ] 183 | /** END TEST CASES **/ 184 | -------------------------------------------------------------------------------- /epaper.gmw.cn.js: -------------------------------------------------------------------------------- 1 | { 2 | "translatorID": "c7f4a1df-1d53-433d-8c04-26fd55791459", 3 | "label": "epaper.gmw.cn", 4 | "creator": "jiaojiaodubai", 5 | "target": "^https://epaper\\.gmw\\.cn/(gmrb|zhdsb)", 6 | "minVersion": "5.0", 7 | "maxVersion": "", 8 | "priority": 100, 9 | "inRepository": true, 10 | "translatorType": 4, 11 | "browserSupport": "gcsibv", 12 | "lastUpdated": "2025-04-08 08:07:01" 13 | } 14 | 15 | /* 16 | ***** BEGIN LICENSE BLOCK ***** 17 | 18 | Copyright © 2025 jiaojiaodubai 19 | 20 | This file is part of Zotero. 21 | 22 | Zotero is free software: you can redistribute it and/or modify 23 | it under the terms of the GNU Affero General Public License as published by 24 | the Free Software Foundation, either version 3 of the License, or 25 | (at your option) any later version. 26 | 27 | Zotero is distributed in the hope that it will be useful, 28 | but WITHOUT ANY WARRANTY; without even the implied warranty of 29 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 30 | GNU Affero General Public License for more details. 31 | 32 | You should have received a copy of the GNU Affero General Public License 33 | along with Zotero. If not, see . 34 | 35 | ***** END LICENSE BLOCK ***** 36 | */ 37 | 38 | 39 | function detectWeb(doc, url) { 40 | // publicationCode_yyyyMMdd_page_order 41 | if (/[a-z]+_\d{8}_\d+-\d+.htm/.test(url)) { 42 | return 'newspaperArticle'; 43 | } 44 | else if (getSearchResults(doc, true)) { 45 | return 'multiple'; 46 | } 47 | return false; 48 | } 49 | 50 | function getSearchResults(doc, checkOnly) { 51 | const items = {}; 52 | let found = false; 53 | const rows = doc.querySelectorAll('#titleList li > a'); 54 | for (const row of rows) { 55 | const href = row.href; 56 | const title = ZU.trimInternal(row.textContent); 57 | if (!href || !title) continue; 58 | if (checkOnly) return true; 59 | found = true; 60 | items[href] = title; 61 | } 62 | return found ? items : false; 63 | } 64 | 65 | async function doWeb(doc, url) { 66 | if (detectWeb(doc, url) === 'multiple') { 67 | const items = await Z.selectItems(getSearchResults(doc, false)); 68 | if (!items) return; 69 | for (const url in items) { 70 | await scrape(await requestDocument(url)); 71 | } 72 | } 73 | else { 74 | await scrape(doc, url); 75 | } 76 | } 77 | 78 | async function scrape(doc, url = doc.location.href) { 79 | if (url.includes('/gmrb/')) { 80 | await scrapeGmrb(doc, url); 81 | } 82 | else if (url.includes('/zhdsb/')) { 83 | await scrapeZhdsb(doc, url); 84 | } 85 | } 86 | 87 | async function scrapeGmrb(doc, url) { 88 | const newItem = new Z.Item('newspaperArticle'); 89 | newItem.title = text(doc, 'h1'); 90 | const pubInfo = text(doc, '.lai > b'); 91 | newItem.publicationTitle = tryMatch(pubInfo, /^《(.+)》/, 1); 92 | newItem.place = '北京'; 93 | newItem.date = tryMatch(pubInfo, /\d{4}年\d{2}月\d{2}/).replace(/\D/g, '-'); 94 | newItem.attachments.push({ 95 | title: 'Snapshot', 96 | document: doc 97 | }); 98 | text(doc, '.lai > :first-child').substring(2).split(' ') 99 | .filter(name => !name.includes('记者')) 100 | .forEach(name => newItem.creators.push(cleanAuthor(name))); 101 | newItem.url = url; 102 | newItem.complete(); 103 | } 104 | 105 | async function scrapeZhdsb(doc, url) { 106 | const newItem = new Z.Item('newspaperArticle'); 107 | newItem.title = text(doc, 'h1'); 108 | const pubInfo = ZU.trimInternal(text(doc, '.lai')); 109 | newItem.publicationTitle = tryMatch(pubInfo, /^《(.+)》/, 1); 110 | newItem.place = '北京'; 111 | newItem.date = tryMatch(pubInfo, /\d{4}年\d{2}月\d{2}/).replace(/\D/g, '-'); 112 | newItem.attachments.push({ 113 | title: 'Snapshot', 114 | document: doc 115 | }); 116 | const content = text(doc, '#articleContent'); 117 | const names = tryMatch(content, /^本报讯(([^)]+))/, 1) 118 | || tryMatch(content, /^■([\p{Unified_Ideograph} ·]+)/u, 1) 119 | || tryMatch(content, /(([\p{Unified_Ideograph} ·]+))$/u, 1); 120 | names.split(' ') 121 | .filter(name => !/记者$/.test(name)) 122 | .forEach(name => newItem.creators.push(cleanAuthor(name.replace(/^记者/, '')))); 123 | newItem.url = url; 124 | newItem.complete(); 125 | } 126 | 127 | function tryMatch(string, pattern, index = 0) { 128 | if (!string) return ''; 129 | const match = string.match(pattern); 130 | return match && match[index] 131 | ? match[index] 132 | : ''; 133 | } 134 | 135 | function cleanAuthor(name, creatorType = 'author') { 136 | return { 137 | firstName: '', 138 | lastName: name, 139 | creatorType, 140 | fildMode: 1 141 | }; 142 | } 143 | 144 | /** BEGIN TEST CASES **/ 145 | var testCases = [ 146 | { 147 | "type": "web", 148 | "url": "https://epaper.gmw.cn/gmrb/html/2025-02/12/nw.D110000gmrb_20250212_4-01.htm", 149 | "items": [ 150 | { 151 | "itemType": "newspaperArticle", 152 | "title": "浙江义乌:“世界超市”有了新“法宝”", 153 | "creators": [ 154 | { 155 | "firstName": "", 156 | "lastName": "陆健", 157 | "creatorType": "author", 158 | "fildMode": 1 159 | }, 160 | { 161 | "firstName": "", 162 | "lastName": "刘习", 163 | "creatorType": "author", 164 | "fildMode": 1 165 | } 166 | ], 167 | "date": "2025-02-12", 168 | "libraryCatalog": "epaper.gmw.cn", 169 | "place": "北京", 170 | "publicationTitle": "光明日报", 171 | "url": "https://epaper.gmw.cn/gmrb/html/2025-02/12/nw.D110000gmrb_20250212_4-01.htm", 172 | "attachments": [ 173 | { 174 | "title": "Snapshot", 175 | "mimeType": "text/html" 176 | } 177 | ], 178 | "tags": [], 179 | "notes": [], 180 | "seeAlso": [] 181 | } 182 | ] 183 | }, 184 | { 185 | "type": "web", 186 | "url": "https://epaper.gmw.cn/gmrb/html/2025-02/13/nbs.D110000gmrb_01.htm", 187 | "items": "multiple" 188 | }, 189 | { 190 | "type": "web", 191 | "url": "https://epaper.gmw.cn/zhdsb/html/2025-04/02/nw.D110000zhdsb_20250402_3-01.htm", 192 | "items": [ 193 | { 194 | "itemType": "newspaperArticle", 195 | "title": "中国科幻正吸引着世界目光", 196 | "creators": [ 197 | { 198 | "firstName": "", 199 | "lastName": "张隽", 200 | "creatorType": "author", 201 | "fildMode": 1 202 | } 203 | ], 204 | "date": "2025-04-02", 205 | "libraryCatalog": "epaper.gmw.cn", 206 | "place": "北京", 207 | "publicationTitle": "中华读书报", 208 | "url": "https://epaper.gmw.cn/zhdsb/html/2025-04/02/nw.D110000zhdsb_20250402_3-01.htm", 209 | "attachments": [ 210 | { 211 | "title": "Snapshot", 212 | "mimeType": "text/html" 213 | } 214 | ], 215 | "tags": [], 216 | "notes": [], 217 | "seeAlso": [] 218 | } 219 | ] 220 | }, 221 | { 222 | "type": "web", 223 | "url": "https://epaper.gmw.cn/zhdsb/html/2025-04/02/nw.D110000zhdsb_20250402_1-17.htm", 224 | "items": [ 225 | { 226 | "itemType": "newspaperArticle", 227 | "title": "绵延百年的投龙故事", 228 | "creators": [ 229 | { 230 | "firstName": "", 231 | "lastName": "魏祝挺", 232 | "creatorType": "author", 233 | "fildMode": 1 234 | } 235 | ], 236 | "date": "2025-04-02", 237 | "libraryCatalog": "epaper.gmw.cn", 238 | "place": "北京", 239 | "publicationTitle": "中华读书报", 240 | "url": "https://epaper.gmw.cn/zhdsb/html/2025-04/02/nw.D110000zhdsb_20250402_1-17.htm", 241 | "attachments": [ 242 | { 243 | "title": "Snapshot", 244 | "mimeType": "text/html" 245 | } 246 | ], 247 | "tags": [], 248 | "notes": [], 249 | "seeAlso": [] 250 | } 251 | ] 252 | }, 253 | { 254 | "type": "web", 255 | "url": "https://epaper.gmw.cn/zhdsb/html/2025-04/02/nw.D110000zhdsb_20250402_2-01.htm", 256 | "items": [ 257 | { 258 | "itemType": "newspaperArticle", 259 | "title": "中国现代文学馆彰显新时代文化自信磅礴力量", 260 | "creators": [ 261 | { 262 | "firstName": "", 263 | "lastName": "夏琪", 264 | "creatorType": "author", 265 | "fildMode": 1 266 | } 267 | ], 268 | "date": "2025-04-02", 269 | "libraryCatalog": "epaper.gmw.cn", 270 | "place": "北京", 271 | "publicationTitle": "中华读书报", 272 | "url": "https://epaper.gmw.cn/zhdsb/html/2025-04/02/nw.D110000zhdsb_20250402_2-01.htm", 273 | "attachments": [ 274 | { 275 | "title": "Snapshot", 276 | "mimeType": "text/html" 277 | } 278 | ], 279 | "tags": [], 280 | "notes": [], 281 | "seeAlso": [] 282 | } 283 | ] 284 | } 285 | ] 286 | /** END TEST CASES **/ 287 | -------------------------------------------------------------------------------- /incoPat.js: -------------------------------------------------------------------------------- 1 | { 2 | "translatorID": "987de44c-15a8-44c8-83ec-1cf03e9f6a32", 3 | "label": "incoPat", 4 | "creator": "jiaojiaodubai", 5 | "target": "^https://www\\.incopat\\.com", 6 | "minVersion": "5.0", 7 | "maxVersion": "", 8 | "priority": 100, 9 | "inRepository": true, 10 | "translatorType": 4, 11 | "browserSupport": "gcsibv", 12 | "lastUpdated": "2024-11-08 00:47:43" 13 | } 14 | 15 | /* 16 | ***** BEGIN LICENSE BLOCK ***** 17 | 18 | Copyright © 2024 jiaojiaodubai 19 | 20 | This file is part of Zotero. 21 | 22 | Zotero is free software: you can redistribute it and/or modify 23 | it under the terms of the GNU Affero General Public License as published by 24 | the Free Software Foundation, either version 3 of the License, or 25 | (at your option) any later version. 26 | 27 | Zotero is distributed in the hope that it will be useful, 28 | but WITHOUT ANY WARRANTY; without even the implied warranty of 29 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 30 | GNU Affero General Public License for more details. 31 | 32 | You should have received a copy of the GNU Affero General Public License 33 | along with Zotero. If not, see . 34 | 35 | ***** END LICENSE BLOCK ***** 36 | */ 37 | 38 | 39 | function detectWeb(doc, url) { 40 | if (url.includes('/detail/' && doc.querySelector('#baseInfoTab > .checked'))) { 41 | return 'patent'; 42 | } 43 | return false; 44 | } 45 | 46 | async function doWeb(doc, url) { 47 | const labels = new Labels(doc, '.fold_con table > tbody > tr'); 48 | const extra = new Extra(); 49 | const newItem = new Z.Item('patent'); 50 | newItem.title = text(doc, '.title > .overflow'); 51 | extra.set('original-title', text(doc, '.translate .con'), true); 52 | newItem.abstractNote = text(doc, '.baseInfo_abstract'); 53 | newItem.place = newItem.country = labels.get('国别'); 54 | newItem.assignee = labels.get('标准化当前权利人'); 55 | newItem.patentNumber = text(doc, '.title > :first-child'); 56 | newItem.filingDate = strToISO(labels.get('申请日')); 57 | newItem.applicationNumber = labels.get('申请号'); 58 | newItem.priorityNumbers = labels.get('优先权号'); 59 | newItem.issueDate = strToISO(labels.get('公开\\(公告\\)日')); 60 | newItem.legalStatus = text(doc, '#color_box > a', 1); 61 | newItem.url = url; 62 | const creatorsZh = labels.get('发明人\\(原始\\)').split('; '); 63 | const creatorsEn = labels.get('发明人\\(翻译\\)').split('; '); 64 | const creatorsExt = []; 65 | for (let i = 0; i < creatorsZh.length; i++) { 66 | const creator = ZU.cleanAuthor(creatorsZh[i], 'inventor'); 67 | if (/[\u4e00-\u9fa5]/.test(creator.lastName)) { 68 | creator.fieldMode = 1; 69 | } 70 | newItem.creators.push(JSON.parse(JSON.stringify(creator))); 71 | if (creatorsEn[i]) { 72 | const creatorEn = ZU.cleanAuthor(ZU.capitalizeName(creatorsEn[i]), 'inventor'); 73 | creator.original = `${creatorEn.firstName} || ${creatorEn.lastName}`; 74 | extra.set('original-author', creator.original, true); 75 | creatorsExt.push(creator); 76 | } 77 | } 78 | if (creatorsExt.some(creator => creator.original)) { 79 | extra.set('creatorsExt', JSON.stringify(creatorsExt)); 80 | } 81 | newItem.attachments.push({ 82 | title: 'Snapshot', 83 | document: doc 84 | }); 85 | newItem.extra = extra.toString(); 86 | newItem.complete(); 87 | } 88 | 89 | class Labels { 90 | constructor(doc, selector) { 91 | this.data = []; 92 | this.emptyElm = doc.createElement('div'); 93 | const nodes = doc.querySelectorAll(selector); 94 | for (const node of nodes) { 95 | // avoid nesting 96 | // avoid empty 97 | if (node.querySelector(selector) || !/\S/.test(node.textContent)) continue; 98 | const elmCopy = node.cloneNode(true); 99 | // avoid empty text 100 | while (![1, 3, 4].includes(elmCopy.firstChild.nodeType) || !/\S/.test(elmCopy.firstChild.textContent)) { 101 | elmCopy.removeChild(elmCopy.firstChild); 102 | if (!elmCopy.firstChild) break; 103 | } 104 | if (elmCopy.childNodes.length > 1) { 105 | const key = elmCopy.removeChild(elmCopy.firstChild).textContent.replace(/\s/g, ''); 106 | this.data.push([key, elmCopy]); 107 | } 108 | else { 109 | const text = ZU.trimInternal(elmCopy.textContent); 110 | const key = tryMatch(text, /^[[【]?.+?[】\]::]/).replace(/\s/g, ''); 111 | elmCopy.textContent = tryMatch(text, /^[[【]?.+?[】\]::]\s*(.+)/, 1); 112 | this.data.push([key, elmCopy]); 113 | } 114 | } 115 | } 116 | 117 | get(label, element = false) { 118 | if (Array.isArray(label)) { 119 | const results = label 120 | .map(aLabel => this.get(aLabel, element)); 121 | const keyVal = element 122 | ? results.find(element => !/^\s*$/.test(element.textContent)) 123 | : results.find(string => string); 124 | return keyVal 125 | ? keyVal 126 | : element 127 | ? this.emptyElm 128 | : ''; 129 | } 130 | const pattern = new RegExp(label, 'i'); 131 | const keyVal = this.data.find(arr => pattern.test(arr[0])); 132 | return keyVal 133 | ? element 134 | ? keyVal[1] 135 | : ZU.trimInternal(keyVal[1].textContent) 136 | : element 137 | ? this.emptyElm 138 | : ''; 139 | } 140 | } 141 | 142 | class Extra { 143 | constructor() { 144 | this.fields = []; 145 | } 146 | 147 | push(key, val, csl = false) { 148 | this.fields.push({ key: key, val: val, csl: csl }); 149 | } 150 | 151 | set(key, val, csl = false) { 152 | const target = this.fields.find(obj => new RegExp(`^${key}$`, 'i').test(obj.key)); 153 | if (target) { 154 | target.val = val; 155 | } 156 | else { 157 | this.push(key, val, csl); 158 | } 159 | } 160 | 161 | get(key) { 162 | const result = this.fields.find(obj => new RegExp(`^${key}$`, 'i').test(obj.key)); 163 | return result 164 | ? result.val 165 | : ''; 166 | } 167 | 168 | toString(history = '') { 169 | this.fields = this.fields.filter(obj => obj.val); 170 | return [ 171 | this.fields.filter(obj => obj.csl).map(obj => `${obj.key}: ${obj.val}`).join('\n'), 172 | history, 173 | this.fields.filter(obj => !obj.csl).map(obj => `${obj.key}: ${obj.val}`).join('\n') 174 | ].filter(obj => obj).join('\n'); 175 | } 176 | } 177 | 178 | function tryMatch(string, pattern, index = 0) { 179 | if (!string) return ''; 180 | const match = string.match(pattern); 181 | return (match && match[index]) 182 | ? match[index] 183 | : ''; 184 | } 185 | 186 | function strToISO(str) { 187 | return /\d{8}/.test(str) 188 | ? `${str.substring(0, 4)}-${str.substring(4, 6)}-${str.substring(6, 8)}` 189 | : ZU.strToISO(str); 190 | } 191 | 192 | /** BEGIN TEST CASES **/ 193 | var testCases = [ 194 | ] 195 | /** END TEST CASES **/ 196 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2017", 4 | "allowSyntheticDefaultImports": true, 5 | "noEmit": true, 6 | "checkJs": true, 7 | "lib": ["dom", "es2017"], 8 | "types": ["index.d.ts"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "translators-check", 3 | "version": "1.0.2", 4 | "description": "Continuous integration of Zotero translators", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "eslint .", 8 | "lint": "eslint", 9 | "lint:linter": "eslint --no-ignore .ci", 10 | "updateTypes": ".ci/updateTypes.mjs" 11 | }, 12 | "author": "kba+pz", 13 | "license": "CC0-1.0", 14 | "repository": { 15 | "url": "https://github.com/zotero/translators/blob/master/.ci/" 16 | }, 17 | "devDependencies": { 18 | "@zotero/eslint-config": "^1.0.8", 19 | "chromedriver": "^122.0.4", 20 | "clarinet": "^0.12.6", 21 | "eslint": "^8.38.0", 22 | "eslint-plugin-zotero-translator": "file:.ci/eslint-plugin-zotero-translator", 23 | "selenium-webdriver": "^4.0.0-alpha.7" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /pm.tsgyun.js: -------------------------------------------------------------------------------- 1 | { 2 | "translatorID": "d8aa03df-6163-47a8-bc27-f23fcdbdba5a", 3 | "label": "pm.tsgyun", 4 | "creator": "jiaojiaodubai", 5 | "target": "^https://pm\\.yuntsg\\.com", 6 | "minVersion": "5.0", 7 | "maxVersion": "", 8 | "priority": 100, 9 | "inRepository": true, 10 | "translatorType": 4, 11 | "browserSupport": "gcsibv", 12 | "lastUpdated": "2024-12-24 13:04:25" 13 | } 14 | 15 | /* 16 | ***** BEGIN LICENSE BLOCK ***** 17 | 18 | Copyright © 2024 jiaojiaodubai 19 | 20 | This file is part of Zotero. 21 | 22 | Zotero is free software: you can redistribute it and/or modify 23 | it under the terms of the GNU Affero General Public License as published by 24 | the Free Software Foundation, either version 3 of the License, or 25 | (at your option) any later version. 26 | 27 | Zotero is distributed in the hope that it will be useful, 28 | but WITHOUT ANY WARRANTY; without even the implied warranty of 29 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 30 | GNU Affero General Public License for more details. 31 | 32 | You should have received a copy of the GNU Affero General Public License 33 | along with Zotero. If not, see . 34 | 35 | ***** END LICENSE BLOCK ***** 36 | */ 37 | 38 | 39 | function detectWeb(doc, url) { 40 | const searchPage = doc.querySelector('.searchPageMain'); 41 | if (searchPage) { 42 | Z.monitorDOMChanges(searchPage, { childList: true, subtree: true }); 43 | } 44 | if (url.includes('/details.html?')) { 45 | return 'journalArticle'; 46 | } 47 | else if (getSearchResults(doc, true)) { 48 | return 'multiple'; 49 | } 50 | return false; 51 | } 52 | 53 | function getSearchResults(doc, checkOnly) { 54 | const items = {}; 55 | let found = false; 56 | const rows = doc.querySelectorAll('.searchList .titleRow'); 57 | for (const row of rows) { 58 | const pmid = attr(row, 'input[type="checkbox"]', 'value'); 59 | const title = text(row, '.titleH'); 60 | if (!pmid || !title) continue; 61 | if (checkOnly) return true; 62 | found = true; 63 | items[`https://pm.yuntsg.com/details.html?pmid=${pmid}`] = title; 64 | } 65 | return found ? items : false; 66 | } 67 | 68 | async function doWeb(doc, url) { 69 | if (detectWeb(doc, url) == 'multiple') { 70 | const items = await Zotero.selectItems(getSearchResults(doc, false)); 71 | if (!items) return; 72 | for (const url of Object.keys(items)) { 73 | await scrape(url); 74 | } 75 | } 76 | else { 77 | await scrape(url); 78 | } 79 | } 80 | 81 | async function scrape(url) { 82 | const parser = new URL(url); 83 | const pmid = parser.searchParams.get('pmid'); 84 | Z.debug(`PMID: ${pmid}`); 85 | const translator = Zotero.loadTranslator('search'); 86 | // Pubmed 87 | translator.setTranslator('3d0231ce-fd4b-478c-b1d3-840389e5b68c'); 88 | translator.setSearch({ itemType: "journalArticle", PMID: pmid }); 89 | translator.setHandler('itemDone', (_obj, item) => { 90 | item.url = `https://pm.yuntsg.com/details.html?pmid=${pmid}`; 91 | item.language = 'en'; 92 | item.complete(); 93 | }); 94 | translator.setHandler('error', () => { }); 95 | await translator.translate(); 96 | } 97 | 98 | /** BEGIN TEST CASES **/ 99 | var testCases = [ 100 | { 101 | "type": "web", 102 | "url": "https://pm.yuntsg.com/details.html?pmid=25833107&key=%E5%BF%83%E8%84%8F", 103 | "items": [ 104 | { 105 | "itemType": "journalArticle", 106 | "title": "The heart's 'little brain' controlling cardiac function in the rabbit", 107 | "creators": [ 108 | { 109 | "firstName": "Kieran E.", 110 | "lastName": "Brack", 111 | "creatorType": "author" 112 | } 113 | ], 114 | "date": "2015-04-01", 115 | "DOI": "10.1113/expphysiol.2014.080168", 116 | "ISSN": "1469-445X", 117 | "abstractNote": "What is the topic of this review? The topic of the review is the intrinsic cardiac nervous system in the rabbit. What advances does it highlight? The anatomy of rabbit intrinsic ganglia is similar to that of other species, including humans. Immunohistochemistry confirms the presence of cholinergic and adrenergic neurones, with a striking arrangement of neuronal nitric oxide synthase-positive cell bodies. Activation of atrial ganglia produces effects on local and remote regions. Heart disease is a primary cause of mortality in the developed world, and it is well recognized that neural mechanisms play an important role in many cardiac pathologies. The role of extrinsic autonomic nerves has traditionally attracted the most attention. However, there is a rich intrinsic innervation of the heart, including numerous cardiac ganglia (ganglionic plexuses), that has the potential to affect cardiac function independently as well as to influence the actions of the extrinsic nerves. To investigate this, an isolated, perfused, innervated rabbit Langendorff heart preparation was considered the best option. Although ganglionic plexuses have been well described for several species, there was no full description of the anatomy and histochemistry of rabbit hearts. To this end, rabbit intrinsic ganglia were located using acetylcholinesterase histology (n = 33 hearts). This revealed six generalized ganglionic regions, defined as a left neuronal complex above the left pulmonary vein, a right neuronal complex around the base of right cranial vein, three scattered in the dorsal right atrium and a region containing numerous ventricular ganglia located on the conus arteriosus. Using immunohistochemistry, neurons were found to contain choline acetyltransferase or tyrosine hydroxylase and/or neuronal nitric oxide synthase in differing amounts (choline acetyltransferase > tyrosine hydroxylase > neuronal nitric oxide synthase). The function of rabbit intrinsic ganglia was investigated using a bolus application of nicotine or electrical stimulation at each of the above sites whilst measuring heart rate and atrioventricular conduction. Nicotine applied to different ganglionic plexuses caused a bradycardia, a tachycardia or a mixture of the two, with the right atrial plexus producing the largest chronotropic responses. Electrical stimulation at these sites induced only a bradycardia. Atrioventricular conduction was modestly changed by nicotine, the main response being a prolongation. Electrical stimulation produced significant prolongation of atrioventricular conduction, particularly when the right neuronal complex was stimulated. These studies show that the intrinsic plexuses of the heart are important and could be crucial for understanding impairments of cardiac function. Additionally, they provide a strong basis from which to progress using the isolated, innervated rabbit heart preparation.", 118 | "extra": "PMID: 25833107\nPMCID: PMC4409095", 119 | "issue": "4", 120 | "journalAbbreviation": "Exp Physiol", 121 | "language": "eng", 122 | "libraryCatalog": "PubMed", 123 | "pages": "348-353", 124 | "publicationTitle": "Experimental Physiology", 125 | "url": "https://pm.yuntsg.com/details.html?pmid=25833107", 126 | "volume": "100", 127 | "attachments": [ 128 | { 129 | "title": "PubMed entry", 130 | "mimeType": "text/html", 131 | "snapshot": false 132 | } 133 | ], 134 | "tags": [ 135 | { 136 | "tag": "Animals" 137 | }, 138 | { 139 | "tag": "Autonomic Nervous System" 140 | }, 141 | { 142 | "tag": "Blood Pressure" 143 | }, 144 | { 145 | "tag": "Feedback, Physiological" 146 | }, 147 | { 148 | "tag": "Heart" 149 | }, 150 | { 151 | "tag": "Heart Conduction System" 152 | }, 153 | { 154 | "tag": "Heart Rate" 155 | }, 156 | { 157 | "tag": "Models, Cardiovascular" 158 | }, 159 | { 160 | "tag": "Models, Neurological" 161 | }, 162 | { 163 | "tag": "Rabbits" 164 | } 165 | ], 166 | "notes": [], 167 | "seeAlso": [] 168 | } 169 | ] 170 | }, 171 | { 172 | "type": "web", 173 | "url": "https://pm.yuntsg.com/searchList.html", 174 | "items": "multiple" 175 | } 176 | ] 177 | /** END TEST CASES **/ 178 | -------------------------------------------------------------------------------- /stats.gov.cn.js: -------------------------------------------------------------------------------- 1 | { 2 | "translatorID": "9f41ad8a-1df6-41d0-928a-0d79d0de7708", 3 | "label": "stats.gov.cn", 4 | "creator": "jiaojiaodubai", 5 | "target": "^https://www\\.stats\\.gov\\.cn", 6 | "minVersion": "5.0", 7 | "maxVersion": "", 8 | "priority": 100, 9 | "inRepository": true, 10 | "translatorType": 4, 11 | "browserSupport": "gcsibv", 12 | "lastUpdated": "2024-04-14 10:26:36" 13 | } 14 | 15 | /* 16 | ***** BEGIN LICENSE BLOCK ***** 17 | 18 | Copyright © 2024 jiaojiaodubai 19 | 20 | This file is part of Zotero. 21 | 22 | Zotero is free software: you can redistribute it and/or modify 23 | it under the terms of the GNU Affero General Public License as published by 24 | the Free Software Foundation, either version 3 of the License, or 25 | (at your option) any later version. 26 | 27 | Zotero is distributed in the hope that it will be useful, 28 | but WITHOUT ANY WARRANTY; without even the implied warranty of 29 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 30 | GNU Affero General Public License for more details. 31 | 32 | You should have received a copy of the GNU Affero General Public License 33 | along with Zotero. If not, see . 34 | 35 | ***** END LICENSE BLOCK ***** 36 | */ 37 | 38 | 39 | function detectWeb(doc, url) { 40 | if (url.endsWith('.html')) { 41 | return 'webpage'; 42 | } 43 | else if (getSearchResults(doc, true)) { 44 | return 'multiple'; 45 | } 46 | return false; 47 | } 48 | 49 | function getSearchResults(doc, checkOnly) { 50 | var items = {}; 51 | var found = false; 52 | var rows = doc.querySelectorAll('a[href$=".html"]'); 53 | for (let row of rows) { 54 | let href = row.href; 55 | let title = row.getAttribute('title') || ZU.trimInternal(row.textContent); 56 | if (!href || !title || !href.includes('www.stats.gov.cn')) continue; 57 | if (checkOnly) return true; 58 | found = true; 59 | items[href] = title; 60 | } 61 | return found ? items : false; 62 | } 63 | 64 | async function doWeb(doc, url) { 65 | if (detectWeb(doc, url) == 'multiple') { 66 | let items = await Zotero.selectItems(getSearchResults(doc, false)); 67 | if (!items) return; 68 | for (let url of Object.keys(items)) { 69 | await scrape(await requestDocument(url)); 70 | } 71 | } 72 | else { 73 | await scrape(doc, url); 74 | } 75 | } 76 | 77 | async function scrape(doc, url = doc.location.href) { 78 | let isEnglishVer = url.includes('/english/'); 79 | let newItem = new Z.Item('webpage'); 80 | var fieldsMap = { 81 | title: { 82 | zh: '.detail-title > h1', 83 | en: 'h1.con_titles', 84 | callback: str => text(doc, str) 85 | }, 86 | websiteTitle: { 87 | zh: '国家统计局', 88 | en: 'National Bureau of Statistics of China', 89 | callback: str => str 90 | }, 91 | date: { 92 | zh: '.detail-title-des > h2:first-child > p', 93 | en: '.info > span:nth-child(2)', 94 | callback: str => ZU.strToISO(text(doc, str)) 95 | }, 96 | language: { 97 | zh: 'zh-CN', 98 | en: 'en-US', 99 | callback: str => str 100 | } 101 | }; 102 | for (let field in fieldsMap) { 103 | let obj = fieldsMap[field]; 104 | newItem[field] = obj.callback(isEnglishVer ? obj.en : obj.zh); 105 | } 106 | newItem.url = url; 107 | newItem.attachments.push({ 108 | title: 'Snapshot', 109 | document: doc 110 | }); 111 | newItem.complete(); 112 | } 113 | 114 | /** BEGIN TEST CASES **/ 115 | var testCases = [ 116 | { 117 | "type": "web", 118 | "url": "https://www.stats.gov.cn/english/PressRelease/202404/t20240409_1954339.html", 119 | "items": [ 120 | { 121 | "itemType": "webpage", 122 | "title": "Market Prices of Important Means of Production in Circulation, March 21-31, 2024", 123 | "creators": [], 124 | "date": "2024-04-04", 125 | "language": "en-US", 126 | "url": "https://www.stats.gov.cn/english/PressRelease/202404/t20240409_1954339.html", 127 | "websiteTitle": "National Bureau of Statistics of China", 128 | "attachments": [ 129 | { 130 | "title": "Snapshot", 131 | "mimeType": "text/html" 132 | } 133 | ], 134 | "tags": [], 135 | "notes": [], 136 | "seeAlso": [] 137 | } 138 | ] 139 | }, 140 | { 141 | "type": "web", 142 | "url": "https://www.stats.gov.cn/sj/zxfb/202404/t20240411_1954447.html", 143 | "items": [ 144 | { 145 | "itemType": "webpage", 146 | "title": "2024年3月份工业生产者出厂价格同比下降2.8%", 147 | "creators": [], 148 | "date": "2024-04-11", 149 | "language": "zh-CN", 150 | "url": "https://www.stats.gov.cn/sj/zxfb/202404/t20240411_1954447.html", 151 | "websiteTitle": "国家统计局", 152 | "attachments": [ 153 | { 154 | "title": "Snapshot", 155 | "mimeType": "text/html" 156 | } 157 | ], 158 | "tags": [], 159 | "notes": [], 160 | "seeAlso": [] 161 | } 162 | ] 163 | }, 164 | { 165 | "type": "web", 166 | "url": "https://www.stats.gov.cn/zsk/?tab=all&siteCode=tjzsk&qt=&sitePath=true", 167 | "items": "multiple" 168 | } 169 | ] 170 | /** END TEST CASES **/ 171 | -------------------------------------------------------------------------------- /xiaoyuzhoufm.js: -------------------------------------------------------------------------------- 1 | { 2 | "translatorID": "9444e3cb-e7d6-4735-b5f5-7d103838f3d9", 3 | "label": "xiaoyuzhouFM", 4 | "creator": "dofine, jiaojiaodubai", 5 | "target": "^https://www\\.xiaoyuzhoufm\\.com", 6 | "minVersion": "5.0", 7 | "maxVersion": "", 8 | "priority": 100, 9 | "inRepository": true, 10 | "translatorType": 4, 11 | "browserSupport": "gcsibv", 12 | "lastUpdated": "2024-04-17 23:09:59" 13 | } 14 | 15 | /* 16 | ***** BEGIN LICENSE BLOCK ***** 17 | 18 | Copyright © 2024 jiaojiaodubai 19 | 20 | This file is part of Zotero. 21 | 22 | Zotero is free software: you can redistribute it and/or modify 23 | it under the terms of the GNU Affero General Public License as published by 24 | the Free Software Foundation, either version 3 of the License, or 25 | (at your option) any later version. 26 | 27 | Zotero is distributed in the hope that it will be useful, 28 | but WITHOUT ANY WARRANTY; without even the implied warranty of 29 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 30 | GNU Affero General Public License for more details. 31 | 32 | You should have received a copy of the GNU Affero General Public License 33 | along with Zotero. If not, see . 34 | 35 | ***** END LICENSE BLOCK ***** 36 | */ 37 | 38 | function detectWeb(doc, url) { 39 | if (url.includes('/episode/')) { 40 | return 'podcast'; 41 | } 42 | else if (getSearchResults(doc, true)) { 43 | return 'multiple'; 44 | } 45 | return false; 46 | } 47 | 48 | function getSearchResults(doc, checkOnly) { 49 | var items = {}; 50 | var found = false; 51 | var rows = doc.querySelectorAll('a.card'); 52 | for (let row of rows) { 53 | let href = row.href; 54 | let title = text(row, '.title'); 55 | if (!href || !title) continue; 56 | if (checkOnly) return true; 57 | found = true; 58 | items[href] = title; 59 | } 60 | return found ? items : false; 61 | } 62 | 63 | async function doWeb(doc, url) { 64 | if (detectWeb(doc, url) == 'multiple') { 65 | let items = await Zotero.selectItems(getSearchResults(doc, false)); 66 | if (!items) return; 67 | for (let url of Object.keys(items)) { 68 | await scrape(await requestDocument(url)); 69 | } 70 | } 71 | else { 72 | await scrape(doc, url); 73 | } 74 | } 75 | 76 | async function scrape(doc, url = doc.location.href) { 77 | const newItem = new Z.Item('podcast'); 78 | const json = JSON.parse(text(doc, '#__NEXT_DATA__')).props.pageProps.episode; 79 | Z.debug(json); 80 | newItem.title = json.title; 81 | newItem.abstractNote = json.description; 82 | newItem.seriesTitle = json.podcast.title; 83 | newItem.audioFileType = tryMatch(json.mediaKey, /\.(\w+?)$/, 1); 84 | newItem.runningTime = (() => { 85 | const date = new Date(null); 86 | date.setSeconds(json.duration); 87 | return `${date.getHours()}:${date.getMinutes()}:${date.getSeconds()}`; 88 | })(); 89 | newItem.url = url; 90 | json.podcast.author.split(';').forEach((str) => { 91 | const creator = ZU.cleanAuthor(str, 'podcaster'); 92 | if (/[\u4e00-\u9fff]/.test(creator.lastName)) { 93 | creator.lastName = creator.firstName + creator.lastName; 94 | creator.fieldMode = 1; 95 | } 96 | newItem.creators.push(creator); 97 | }); 98 | newItem.attachments.push({ 99 | url: json.media.source.url, 100 | title: 'Audio', 101 | mimeType: json.media.mimeType 102 | }); 103 | newItem.complete(); 104 | } 105 | 106 | /** 107 | * Attempts to get the part of the pattern described from the character, 108 | * and returns an empty string if not match. 109 | * @param {String} string 110 | * @param {RegExp} pattern 111 | * @param {Number} index 112 | * @returns 113 | */ 114 | function tryMatch(string, pattern, index = 0) { 115 | if (!string) return ''; 116 | let match = string.match(pattern); 117 | return (match && match[index]) 118 | ? match[index] 119 | : ''; 120 | } 121 | 122 | /** BEGIN TEST CASES **/ 123 | var testCases = [ 124 | { 125 | "type": "web", 126 | "url": "https://www.xiaoyuzhoufm.com/podcast/62b42afbc9eae959ff1df95b", 127 | "items": "multiple" 128 | }, 129 | { 130 | "type": "web", 131 | "url": "https://www.xiaoyuzhoufm.com/episode/65c35d5e0bef6c2074ba73a5", 132 | "items": [ 133 | { 134 | "itemType": "podcast", 135 | "title": "号外:提前给大家拜年啦!", 136 | "creators": [ 137 | { 138 | "firstName": "", 139 | "lastName": "宇宙乘客", 140 | "creatorType": "podcaster", 141 | "fieldMode": 1 142 | } 143 | ], 144 | "abstractNote": "大家好,好久不见!今天坐在这里也是想大家提前说一声春节祝福,给大家拜个早年。在过去的 4 个月,有很多朋友留言或者私信催更,这些留言和私信我们都看到了。过去这几个月,我们个人的生活都发生了一些变化,我自己也是经历了一次比较剧烈的情绪问题,好在这一切都结束了,我自己的情绪也慢慢好起来。\n到这个月的 14 号,宇宙乘客就要开始第四个年头了,非常感谢大家一直的支持,你们的每一条留言我都有看到。新的一年,我们也想做一点和之前不一样的内容给大家,所以,宇宙乘客的更新还得再等等。 不过,我自己开了一档 solo 播客叫「三文鱼」,记录了我最近经历的事情、感悟和一些计划,也会帮助到很多人来认识自己、关注自己的情绪、改善自己的人际关系,提升自己的行动力。欢迎大家订阅、收听。\n还有一件事,如果近期有朋友在爱发电购买我们的付费内容,没有收到兑换码,请添加我的微信:holauntie(备注爱发电),我来给大家亲自发送兑换码,如果大家想购买付费,可以直接在小宇宙或者公众号购买,支付后不需要兑换可以直接收听。\n接下来,到了煽情的节点了,这一年发生了很多事情,也失去了很多,但同时也得到了很多。我想每个人在深夜思考自己这一年的时候,一定是有说有笑但不可避免的有痛苦有眼泪有委屈,这都是生活的组成,正是因为这些吉光片羽的时刻,才让我们活的更加深刻。\n在这几天的假期里,希望我们都可以好好休息,把那些暂时还没有解决的事情暂时放下,那些还没有修复好的关系暂时放下,那些还没有想明白的事情暂时放下,彻底的让自己好好休息一下,调整好身体和心理,春节后我们再满怀热情的大干一场,时间还有很多,我们不要着急,一切事情都会朝我们期待的方向进行。\n再次感谢大家对宇宙乘客的支持和陪伴,祝大家春节快乐,龙年大吉!", 145 | "audioFileType": "m4a", 146 | "runningTime": "8:5:48", 147 | "seriesTitle": "宇宙乘客", 148 | "url": "https://www.xiaoyuzhoufm.com/episode/65c35d5e0bef6c2074ba73a5", 149 | "attachments": [ 150 | { 151 | "title": "Audio", 152 | "mimeType": "audio/mp4" 153 | } 154 | ], 155 | "tags": [], 156 | "notes": [], 157 | "seeAlso": [] 158 | } 159 | ] 160 | } 161 | ] 162 | /** END TEST CASES **/ 163 | --------------------------------------------------------------------------------