[
  {
    "path": ".babelrc",
    "content": "{\n  \"presets\": [\"@babel/preset-env\"]\n}\n"
  },
  {
    "path": ".editorconfig",
    "content": "# http://editorconfig.org\nroot = true\n\n[*]\nindent_style = space\nindent_size = 2\ncharset = utf-8\ntrim_trailing_whitespace = true\ninsert_final_newline = true\n\n[*.md]\ntrim_trailing_whitespace = false\n"
  },
  {
    "path": ".eslintrc.json",
    "content": "{\n  \"parser\": \"babel-eslint\",\n  \"globals\": {\n    \"Promise\": true\n  },\n  \"env\": {\n    \"browser\": true,\n    \"node\": true\n  },\n  \"extends\": [\"plugin:prettier/recommended\"],\n  \"rules\": {\n    \"arrow-parens\": [\n      2,\n      \"as-needed\"\n    ],\n    \"keyword-spacing\": 2,\n    \"no-extra-semi\": 2,\n    \"no-undef\": 2,\n    \"no-unused-vars\": 2,\n    \"no-var\": 2,\n    \"prettier/prettier\": [\n      2,\n      {\n        \"printWidth\": 120,\n        \"singleQuote\": true,\n        \"trailingComma\": \"none\"\n      }\n    ],\n    \"semi\": [\n      2,\n      \"always\"\n    ]\n  }\n}\n"
  },
  {
    "path": ".gitignore",
    "content": "/node_modules/\n/dist/\n*.log\nplay.js\n"
  },
  {
    "path": ".npmignore",
    "content": ".gitignore\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: node_js\nnode_js:\n  - '12'\n  - '10'\n"
  },
  {
    "path": "README.md",
    "content": "# anki-apkg-export\n\n[![Build Status](https://travis-ci.org/repeat-space/anki-apkg-export.svg?branch=master)](https://travis-ci.org/repeat-space/anki-apkg-export)\n\nUniversal module for generating decks for Anki.\n\nPort of the Ruby gem https://github.com/albertzak/anki2\n\n## Install\n\n```\n$ npm install anki-apkg-export --save\n```\n\n## Usage\n\n### server\n\n```js\nconst fs = require('fs');\nconst AnkiExport = require('anki-apkg-export').default;\n\nconst apkg = new AnkiExport('deck-name');\n\napkg.addMedia('anki.png', fs.readFileSync('anki.png'));\n\napkg.addCard('card #1 front', 'card #1 back');\napkg.addCard('card #2 front', 'card #2 back', { tags: ['nice', 'better card'] });\napkg.addCard('card #3 with image <img src=\"anki.png\" />', 'card #3 back');\n\napkg\n  .save()\n  .then(zip => {\n    fs.writeFileSync('./output.apkg', zip, 'binary');\n    console.log(`Package has been generated: output.pkg`);\n  })\n  .catch(err => console.log(err.stack || err));\n```\n\n### browser\n\nIntended to be used with [`webpack`](https://github.com/webpack/webpack)\n\n```js\nconst webpack = require('webpack');\n\nmodule.exports = {\n  entry: './index.js',\n  module: {\n    loaders: [\n      {\n        test: /\\.js$/,\n        exclude: /node_modules/,\n        loader: 'babel'\n      },\n    ]\n  },\n  plugins: [\n    new webpack.DefinePlugin({\n      'process.env': {\n        NODE_ENV: JSON.stringify(process.env.NODE_ENV || 'development')\n      },\n    })\n  ],\n  output: {\n    path: __dirname,\n    filename: 'bundle.js'\n  }\n};\n```\n\nRequired loaders:\n\n- [`script-loader`](https://github.com/webpack/script-loader)\n\n```js\nimport { saveAs } from 'file-saver';\nimport AnkiExport from 'anki-apkg-export';\n\nconst apkg = new AnkiExport('deck-name');\n\n// could be a File from <input /> or a Blob from fetch\n// take a look at the example folder for a complete overview\napkg.addMedia('anki.png', file);\n\napkg.addCard('card #1 front', 'card #1 back');\napkg.addCard('card #2 front', 'card #2 back', { tags: ['nice', 'better card'] });\napkg.addCard('card #3 with image <img src=\"anki.png\" />', 'card #3 back');\n\napkg\n  .save()\n  .then(zip => {\n    saveAs(zip, 'output.apkg');\n  })\n  .catch(err => console.log(err.stack || err));\n```\n\n## Examples\n\n- [server from above](examples/server)\n- [browser from above](examples/browser)\n- [browser usage with media attachments via ajax](examples/browser-media-ajax)\n- [browser usage with media attachments via <form />](examples/browser-media-file-input)\n\n## Changelog\n\n- `v4.0.0` - expose template variables (frontside, backside and css)\n- `v3.1.0` - make setting APP_ENV optional\n- `v3.0.0` - add tags, ES6 refactor (breaking)\n- `v2.0.0` - add media support, update jszip dependency (breaking)\n- `v1.0.0` - initial rewrite\n\n## Tips\n\n- [issue#25](https://github.com/ewnd9/anki-apkg-export/issues/25) - Dealing with `sql.js` memory limits\n\n## Related\n\n- [apkg format documentation](http://decks.wikia.com/wiki/Anki_APKG_format_documentation)\n- [anki-apkg-export-cli](https://github.com/ewnd9/anki-apkg-export-cli) - CLI for this module\n- [anki-apkg-export-app](https://github.com/ewnd9/anki-apkg-export-app) - Simple web app to generate cards online\n\n## License\n\nMIT © [ewnd9](http://ewnd9.com)\n"
  },
  {
    "path": "ava.config.js",
    "content": "export default {\n  require: ['@babel/register', '@babel/polyfill']\n};\n"
  },
  {
    "path": "examples/browser/.babelrc",
    "content": "{\n  \"presets\": [\"es2015\"]\n}\n"
  },
  {
    "path": "examples/browser/.gitignore",
    "content": "node_modules\nbundle.js\n"
  },
  {
    "path": "examples/browser/index.html",
    "content": "<script src=\"/bundle.js\"></script>\n"
  },
  {
    "path": "examples/browser/index.js",
    "content": "import { saveAs } from 'file-saver';\n\nimport AnkiExport from '../../src';\n// import AnkiExport from 'anki-apkg-export';\n\nconst apkg = new AnkiExport('deck-name');\n\napkg.addCard('card #1 front', 'card #1 back');\napkg.addCard('card #2 front', 'card #2 back');\n\napkg\n  .save()\n  .then(zip => {\n    saveAs(zip, 'output.apkg');\n  })\n  .catch(err => console.log(err.stack || err));\n"
  },
  {
    "path": "examples/browser/package.json",
    "content": "{\n  \"private\": true,\n  \"scripts\": {\n    \"build\": \"webpack\"\n  },\n  \"dependencies\": {\n    \"anki-apkg-export\": \"^3.0.1\",\n    \"babel-loader\": \"^6.2.4\",\n    \"babel-preset-es2015\": \"^6.6.0\",\n    \"file-saver\": \"^1.3.3\"\n  },\n  \"devDependencies\": {\n    \"script-loader\": \"^0.6.1\",\n    \"webpack\": \"^1.12.14\"\n  }\n}\n"
  },
  {
    "path": "examples/browser/webpack.config.js",
    "content": "'use strict';\n\nconst webpack = require('webpack');\n\nmodule.exports = {\n  entry: './index.js',\n  module: {\n    loaders: [\n      {\n        test: /\\.js$/,\n        exclude: /node_modules/,\n        loader: 'babel'\n      },\n    ]\n  },\n  plugins: [\n    new webpack.DefinePlugin({\n      'process.env': {\n        NODE_ENV: JSON.stringify(process.env.NODE_ENV || 'development'),\n        APP_ENV: JSON.stringify('browser')\n      },\n    })\n  ],\n  output: {\n    path: __dirname,\n    filename: 'bundle.js'\n  }\n};\n"
  },
  {
    "path": "examples/browser-media-ajax/.babelrc",
    "content": "{\n  \"presets\": [\"es2015\"]\n}\n"
  },
  {
    "path": "examples/browser-media-ajax/.gitignore",
    "content": "node_modules\nbundle.js\n"
  },
  {
    "path": "examples/browser-media-ajax/index.html",
    "content": "<script src=\"/bundle.js\"></script>\n"
  },
  {
    "path": "examples/browser-media-ajax/index.js",
    "content": "import { saveAs } from 'file-saver';\n\nimport AnkiExport from '../../src';\n// import AnkiExport from 'anki-apkg-export';\n\nconst apkg = new AnkiExport('deck-name-ajax');\n\nconst params = {\n  method: 'GET'\n};\n\nfetch('https://raw.githubusercontent.com/ewnd9/anki-apkg-export/39ebdd664ab23b5237eee95b7dd88c457e263a20/example/assets/anki.png', params)\n  .then(function(response) {\n    return response.blob();\n  })\n  .then(function(myBlob) {\n    apkg.addMedia('anki.png', myBlob);\n    apkg.addCard('card #1 with image <img src=\"anki.png\" />', 'card #1 back');\n\n    return apkg.save()\n  })\n  .then(function(zip) {\n    saveAs(zip, 'output.apkg');\n  })\n  .catch(err => console.log(err.stack || err));\n"
  },
  {
    "path": "examples/browser-media-ajax/package.json",
    "content": "{\n  \"private\": true,\n  \"scripts\": {\n    \"build\": \"webpack\"\n  },\n  \"dependencies\": {\n    \"anki-apkg-export\": \"^3.0.1\",\n    \"babel-loader\": \"^6.2.4\",\n    \"babel-preset-es2015\": \"^6.6.0\",\n    \"file-saver\": \"^1.3.3\"\n  },\n  \"devDependencies\": {\n    \"script-loader\": \"^0.6.1\",\n    \"webpack\": \"^1.12.14\"\n  }\n}\n"
  },
  {
    "path": "examples/browser-media-ajax/webpack.config.js",
    "content": "'use strict';\n\nconst webpack = require('webpack');\n\nmodule.exports = {\n  entry: './index.js',\n  module: {\n    loaders: [\n      {\n        test: /\\.js$/,\n        exclude: /node_modules/,\n        loader: 'babel'\n      },\n    ]\n  },\n  plugins: [\n    new webpack.DefinePlugin({\n      'process.env': {\n        NODE_ENV: JSON.stringify(process.env.NODE_ENV || 'development'),\n        APP_ENV: JSON.stringify('browser')\n      },\n    })\n  ],\n  output: {\n    path: __dirname,\n    filename: 'bundle.js'\n  }\n};\n"
  },
  {
    "path": "examples/browser-media-file-input/.babelrc",
    "content": "{\n  \"presets\": [\"es2015\"]\n}\n"
  },
  {
    "path": "examples/browser-media-file-input/.gitignore",
    "content": "node_modules\nbundle.js\n"
  },
  {
    "path": "examples/browser-media-file-input/index.html",
    "content": "<input id=\"input\" type=\"file\" />\n<script src=\"/bundle.js\"></script>\n"
  },
  {
    "path": "examples/browser-media-file-input/index.js",
    "content": "import { saveAs } from 'file-saver';\n\nimport AnkiExport from '../../src';\n// import AnkiExport from 'anki-apkg-export';\n\nconst apkg = new AnkiExport('deck-name');\nconst input = document.getElementById('input');\n\ninput.onchange = function(e) {\n  const reader = new FileReader();\n  const filename = e.target.files[0].name;\n\n  reader.onload = function(e) {\n    const file = e.target.result;\n\n    apkg.addMedia('anki.png', file);\n    apkg.addCard('card #1 with image <img src=\"anki.png\" />', 'card #1 back');\n\n    apkg\n      .save()\n      .then(zip => {\n        saveAs(zip, 'output.apkg');\n      })\n      .catch(err => console.log(err.stack || err));\n  };\n\n  reader.readAsArrayBuffer(e.target.files[0]);\n};\n"
  },
  {
    "path": "examples/browser-media-file-input/package.json",
    "content": "{\n  \"private\": true,\n  \"scripts\": {\n    \"build\": \"webpack\"\n  },\n  \"dependencies\": {\n    \"anki-apkg-export\": \"^3.0.1\",\n    \"babel-loader\": \"^6.2.4\",\n    \"babel-preset-es2015\": \"^6.6.0\",\n    \"file-saver\": \"^1.3.3\"\n  },\n  \"devDependencies\": {\n    \"script-loader\": \"^0.6.1\",\n    \"webpack\": \"^1.12.14\"\n  }\n}\n"
  },
  {
    "path": "examples/browser-media-file-input/webpack.config.js",
    "content": "'use strict';\n\nconst webpack = require('webpack');\n\nmodule.exports = {\n  entry: './index.js',\n  module: {\n    loaders: [\n      {\n        test: /\\.js$/,\n        exclude: /node_modules/,\n        loader: 'babel'\n      },\n    ]\n  },\n  plugins: [\n    new webpack.DefinePlugin({\n      'process.env': {\n        NODE_ENV: JSON.stringify(process.env.NODE_ENV || 'development'),\n        APP_ENV: JSON.stringify('browser')\n      },\n    })\n  ],\n  output: {\n    path: __dirname,\n    filename: 'bundle.js'\n  }\n};\n"
  },
  {
    "path": "examples/server/server.js",
    "content": "'use strict';\n\nconst fs = require('fs');\n\n// const { default: AnkiExport } = require('anki-apkg-export');\nconst { default: AnkiExport } = require('../../dist');\n\nconst apkg = new AnkiExport('deck-name-node');\n\napkg.addMedia('anki.png', fs.readFileSync('../assets/anki.png'));\n\napkg.addCard('card #1 front', 'card #1 back');\napkg.addCard('card #2 front', 'card #2 back');\napkg.addCard('card #3 with image <img src=\"anki.png\" />', 'card #3 back');\n\napkg\n  .save()\n  .then(zip => {\n    fs.writeFileSync('./output.apkg', zip, 'binary');\n    console.log(`Package has been generated: output.apkg`);\n  })\n  .catch(err => console.log(err.stack || err));\n"
  },
  {
    "path": "husky.config.js",
    "content": "module.exports = {\n  hooks: {\n    'pre-commit': 'npm test'\n  }\n};\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"anki-apkg-export\",\n  \"description\": \"Generate decks for Anki (spaced repetition software)\",\n  \"version\": \"4.0.3\",\n  \"main\": \"dist/index.js\",\n  \"dependencies\": {\n    \"jszip\": \"^3.2.2\",\n    \"sha1\": \"^1.1.1\",\n    \"sql.js\": \"^0.5.0\"\n  },\n  \"devDependencies\": {\n    \"@babel/cli\": \"^7.0.0\",\n    \"@babel/core\": \"^7.0.0\",\n    \"@babel/polyfill\": \"^7.0.0\",\n    \"@babel/preset-env\": \"^7.0.0\",\n    \"@babel/register\": \"^7.0.0\",\n    \"arraybuffer-equal\": \"1.0.4\",\n    \"ava\": \"^2.4.0\",\n    \"babel-eslint\": \"^10.0.3\",\n    \"eslint\": \"^6.8.0\",\n    \"eslint-config-prettier\": \"^6.9.0\",\n    \"eslint-plugin-prettier\": \"^3.1.2\",\n    \"husky\": \"^3.1.0\",\n    \"lodash.sortby\": \"4.7.0\",\n    \"mkdirp\": \"0.5.1\",\n    \"pify\": \"^4.0.1\",\n    \"prettier\": \"^1.19.1\",\n    \"proxyquire\": \"^2.1.3\",\n    \"sinon\": \"^8.0.2\",\n    \"sqlite3\": \"4.1.1\"\n  },\n  \"author\": \"ewnd9 <ewndnine@gmail.com>\",\n  \"keywords\": [\n    \"anki\",\n    \"spaced repetition software\",\n    \"webpack\"\n  ],\n  \"license\": \"MIT\",\n  \"preferGlobal\": \"true\",\n  \"repository\": {\n    \"type\": \"git\",\n    \"url\": \"git+https://github.com/ewnd9/anki-apkg-export.git\"\n  },\n  \"scripts\": {\n    \"build\": \"babel -d dist src\",\n    \"build:watch\": \"babel -w -d dist src\",\n    \"lint\": \"eslint 'src/**/*.js' 'test/**/*.js'\",\n    \"postpublish\": \"rm -rf dist\",\n    \"prepare\": \"yarn run build\",\n    \"test\": \"yarn run lint && ava\",\n    \"test:watch\": \"yarn run test -- --watch\"\n  }\n}\n"
  },
  {
    "path": "src/exporter.js",
    "content": "import sha1 from 'sha1';\nimport Zip from 'jszip';\n\nexport default class {\n  constructor(deckName, { template, sql }) {\n    this.db = new sql.Database();\n    this.db.run(template);\n\n    const now = Date.now();\n    const topDeckId = this._getId('cards', 'did', now);\n    const topModelId = this._getId('notes', 'mid', now);\n\n    this.deckName = deckName;\n    this.zip = new Zip();\n    this.media = [];\n    this.topDeckId = topDeckId;\n    this.topModelId = topModelId;\n    this.separator = '\\u001F';\n\n    const decks = this._getInitialRowValue('col', 'decks');\n    const deck = getLastItem(decks);\n    deck.name = this.deckName;\n    deck.id = topDeckId;\n    decks[topDeckId + ''] = deck;\n    this._update('update col set decks=:decks where id=1', { ':decks': JSON.stringify(decks) });\n\n    const models = this._getInitialRowValue('col', 'models');\n    const model = getLastItem(models);\n    model.name = this.deckName;\n    model.did = this.topDeckId;\n    model.id = topModelId;\n    models[`${topModelId}`] = model;\n    this._update('update col set models=:models where id=1', { ':models': JSON.stringify(models) });\n  }\n\n  save(options) {\n    const { zip, db, media } = this;\n    const binaryArray = db.export();\n    const mediaObj = media.reduce((prev, curr, idx) => {\n      prev[idx] = curr.filename;\n      return prev;\n    }, {});\n\n    zip.file('collection.anki2', new Buffer(binaryArray));\n    zip.file('media', JSON.stringify(mediaObj));\n\n    media.forEach((item, i) => zip.file(i, item.data));\n\n    if (process.env.APP_ENV === 'browser' || typeof window !== 'undefined') {\n      return zip.generateAsync(Object.assign({}, { type: 'blob' }, options));\n    } else {\n      return zip.generateAsync(\n        Object.assign(\n          {},\n          {\n            type: 'nodebuffer',\n            base64: false,\n            compression: 'DEFLATE'\n          },\n          options\n        )\n      );\n    }\n  }\n\n  addMedia(filename, data) {\n    this.media.push({ filename, data });\n  }\n\n  addCard(front, back, { tags } = {}) {\n    const { topDeckId, topModelId, separator } = this;\n    const now = Date.now();\n    const note_guid = this._getNoteGuid(topDeckId, front, back);\n    const note_id = this._getNoteId(note_guid, now);\n\n    let strTags = '';\n    if (typeof tags === 'string') {\n      strTags = tags;\n    } else if (Array.isArray(tags)) {\n      strTags = this._tagsToStr(tags);\n    }\n\n    this._update('insert or replace into notes values(:id,:guid,:mid,:mod,:usn,:tags,:flds,:sfld,:csum,:flags,:data)', {\n      ':id': note_id, // integer primary key,\n      ':guid': note_guid, // text not null,\n      ':mid': topModelId, // integer not null,\n      ':mod': this._getId('notes', 'mod', now), // integer not null,\n      ':usn': -1, // integer not null,\n      ':tags': strTags, // text not null,\n      ':flds': front + separator + back, // text not null,\n      ':sfld': front, // integer not null,\n      ':csum': this._checksum(front + separator + back), //integer not null,\n      ':flags': 0, // integer not null,\n      ':data': '' // text not null,\n    });\n\n    return this._update(\n      'insert or replace into cards values(:id,:nid,:did,:ord,:mod,:usn,:type,:queue,:due,:ivl,:factor,:reps,:lapses,:left,:odue,:odid,:flags,:data)',\n      {\n        ':id': this._getCardId(note_id, now), // integer primary key,\n        ':nid': note_id, // integer not null,\n        ':did': topDeckId, // integer not null,\n        ':ord': 0, // integer not null,\n        ':mod': this._getId('cards', 'mod', now), // integer not null,\n        ':usn': -1, // integer not null,\n        ':type': 0, // integer not null,\n        ':queue': 0, // integer not null,\n        ':due': 179, // integer not null,\n        ':ivl': 0, // integer not null,\n        ':factor': 0, // integer not null,\n        ':reps': 0, // integer not null,\n        ':lapses': 0, // integer not null,\n        ':left': 0, // integer not null,\n        ':odue': 0, // integer not null,\n        ':odid': 0, // integer not null,\n        ':flags': 0, // integer not null,\n        ':data': '' // text not null\n      }\n    );\n  }\n\n  _update(query, obj) {\n    this.db.prepare(query).getAsObject(obj);\n  }\n\n  _getInitialRowValue(table, column = 'id') {\n    const query = `select ${column} from ${table}`;\n    return this._getFirstVal(query);\n  }\n\n  _checksum(str) {\n    return parseInt(sha1(str).substr(0, 8), 16);\n  }\n\n  _getFirstVal(query) {\n    return JSON.parse(this.db.exec(query)[0].values[0]);\n  }\n\n  _tagsToStr(tags = []) {\n    return ' ' + tags.map(tag => tag.replace(/ /g, '_')).join(' ') + ' ';\n  }\n\n  _getId(table, col, ts) {\n    const query = `SELECT ${col} from ${table} WHERE ${col} >= :ts ORDER BY ${col} DESC LIMIT 1`;\n    const rowObj = this.db.prepare(query).getAsObject({ ':ts': ts });\n\n    return rowObj[col] ? +rowObj[col] + 1 : ts;\n  }\n\n  _getNoteId(guid, ts) {\n    const query = `SELECT id from notes WHERE guid = :guid ORDER BY id DESC LIMIT 1`;\n    const rowObj = this.db.prepare(query).getAsObject({ ':guid': guid });\n\n    return rowObj.id || this._getId('notes', 'id', ts);\n  }\n\n  _getNoteGuid(topDeckId, front, back) {\n    return sha1(`${topDeckId}${front}${back}`);\n  }\n\n  _getCardId(note_id, ts) {\n    const query = `SELECT id from cards WHERE nid = :note_id ORDER BY id DESC LIMIT 1`;\n    const rowObj = this.db.prepare(query).getAsObject({ ':note_id': note_id });\n\n    return rowObj.id || this._getId('cards', 'id', ts);\n  }\n}\n\nexport const getLastItem = obj => {\n  const keys = Object.keys(obj);\n  const lastKey = keys[keys.length - 1];\n\n  const item = obj[lastKey];\n  delete obj[lastKey];\n\n  return item;\n};\n"
  },
  {
    "path": "src/index.js",
    "content": "import Exporter from './exporter';\nimport createTemplate from './template';\n\nlet sql;\n\nif (process.env.APP_ENV === 'browser' || typeof window !== 'undefined') {\n  require('script-loader!sql.js');\n  sql = window.SQL;\n} else {\n  sql = require('sql.js');\n}\n\nexport { Exporter };\n\nexport default function(deckName, template) {\n  return new Exporter(deckName, {\n    template: createTemplate(template),\n    sql\n  });\n}\n"
  },
  {
    "path": "src/template.js",
    "content": "export default function createTemplate({\n  questionFormat = '{{Front}}',\n  answerFormat = '{{FrontSide}}\\n\\n<hr id=\"answer\">\\n\\n{{Back}}',\n  css = '.card {\\n font-family: arial;\\n font-size: 20px;\\n text-align: center;\\n color: black;\\nbackground-color: white;\\n}\\n'\n} = {}) {\n  const conf = {\n    nextPos: 1,\n    estTimes: true,\n    activeDecks: [1],\n    sortType: 'noteFld',\n    timeLim: 0,\n    sortBackwards: false,\n    addToCur: true,\n    curDeck: 1,\n    newBury: true,\n    newSpread: 0,\n    dueCounts: true,\n    curModel: '1435645724216',\n    collapseTime: 1200\n  };\n\n  const models = {\n    1388596687391: {\n      veArs: [],\n      name: 'Basic-f15d2',\n      tags: ['Tag'],\n      did: 1435588830424,\n      usn: -1,\n      req: [[0, 'all', [0]]],\n      flds: [\n        {\n          name: 'Front',\n          media: [],\n          sticky: false,\n          rtl: false,\n          ord: 0,\n          font: 'Arial',\n          size: 20\n        },\n        {\n          name: 'Back',\n          media: [],\n          sticky: false,\n          rtl: false,\n          ord: 1,\n          font: 'Arial',\n          size: 20\n        }\n      ],\n      sortf: 0,\n      latexPre:\n        '\\\\documentclass[12pt]{article}\\n\\\\special{papersize=3in,5in}\\n\\\\usepackage[utf8]{inputenc}\\n\\\\usepackage{amssymb,amsmath}\\n\\\\pagestyle{empty}\\n\\\\setlength{\\\\parindent}{0in}\\n\\\\begin{document}\\n',\n      tmpls: [\n        {\n          name: 'Card 1',\n          qfmt: questionFormat,\n          did: null,\n          bafmt: '',\n          afmt: answerFormat,\n          ord: 0,\n          bqfmt: ''\n        }\n      ],\n      latexPost: '\\\\end{document}',\n      type: 0,\n      id: 1388596687391,\n      css,\n      mod: 1435645658\n    }\n  };\n\n  const decks = {\n    1: {\n      desc: '',\n      name: 'Default',\n      extendRev: 50,\n      usn: 0,\n      collapsed: false,\n      newToday: [0, 0],\n      timeToday: [0, 0],\n      dyn: 0,\n      extendNew: 10,\n      conf: 1,\n      revToday: [0, 0],\n      lrnToday: [0, 0],\n      id: 1,\n      mod: 1435645724\n    },\n    1435588830424: {\n      desc: '',\n      name: 'Template',\n      extendRev: 50,\n      usn: -1,\n      collapsed: false,\n      newToday: [545, 0],\n      timeToday: [545, 0],\n      dyn: 0,\n      extendNew: 10,\n      conf: 1,\n      revToday: [545, 0],\n      lrnToday: [545, 0],\n      id: 1435588830424,\n      mod: 1435588830\n    }\n  };\n\n  const dconf = {\n    1: {\n      name: 'Default',\n      replayq: true,\n      lapse: {\n        leechFails: 8,\n        minInt: 1,\n        delays: [10],\n        leechAction: 0,\n        mult: 0\n      },\n      rev: {\n        perDay: 100,\n        fuzz: 0.05,\n        ivlFct: 1,\n        maxIvl: 36500,\n        ease4: 1.3,\n        bury: true,\n        minSpace: 1\n      },\n      timer: 0,\n      maxTaken: 60,\n      usn: 0,\n      new: {\n        perDay: 20,\n        delays: [1, 10],\n        separate: true,\n        ints: [1, 4, 7],\n        initialFactor: 2500,\n        bury: true,\n        order: 1\n      },\n      mod: 0,\n      id: 1,\n      autoplay: true\n    }\n  };\n\n  return `\n    PRAGMA foreign_keys=OFF;\n    BEGIN TRANSACTION;\n    CREATE TABLE col (\n        id              integer primary key,\n        crt             integer not null,\n        mod             integer not null,\n        scm             integer not null,\n        ver             integer not null,\n        dty             integer not null,\n        usn             integer not null,\n        ls              integer not null,\n        conf            text not null,\n        models          text not null,\n        decks           text not null,\n        dconf           text not null,\n        tags            text not null\n    );\n    INSERT INTO \"col\" VALUES(\n      1,\n      1388548800,\n      1435645724219,\n      1435645724215,\n      11,\n      0,\n      0,\n      0,\n      '${JSON.stringify(conf)}',\n      '${JSON.stringify(models)}',\n      '${JSON.stringify(decks)}',\n      '${JSON.stringify(dconf)}',\n      '{}'\n    );\n    CREATE TABLE notes (\n        id              integer primary key,   /* 0 */\n        guid            text not null,         /* 1 */\n        mid             integer not null,      /* 2 */\n        mod             integer not null,      /* 3 */\n        usn             integer not null,      /* 4 */\n        tags            text not null,         /* 5 */\n        flds            text not null,         /* 6 */\n        sfld            integer not null,      /* 7 */\n        csum            integer not null,      /* 8 */\n        flags           integer not null,      /* 9 */\n        data            text not null          /* 10 */\n    );\n    CREATE TABLE cards (\n        id              integer primary key,   /* 0 */\n        nid             integer not null,      /* 1 */\n        did             integer not null,      /* 2 */\n        ord             integer not null,      /* 3 */\n        mod             integer not null,      /* 4 */\n        usn             integer not null,      /* 5 */\n        type            integer not null,      /* 6 */\n        queue           integer not null,      /* 7 */\n        due             integer not null,      /* 8 */\n        ivl             integer not null,      /* 9 */\n        factor          integer not null,      /* 10 */\n        reps            integer not null,      /* 11 */\n        lapses          integer not null,      /* 12 */\n        left            integer not null,      /* 13 */\n        odue            integer not null,      /* 14 */\n        odid            integer not null,      /* 15 */\n        flags           integer not null,      /* 16 */\n        data            text not null          /* 17 */\n    );\n    CREATE TABLE revlog (\n        id              integer primary key,\n        cid             integer not null,\n        usn             integer not null,\n        ease            integer not null,\n        ivl             integer not null,\n        lastIvl         integer not null,\n        factor          integer not null,\n        time            integer not null,\n        type            integer not null\n    );\n    CREATE TABLE graves (\n        usn             integer not null,\n        oid             integer not null,\n        type            integer not null\n    );\n    ANALYZE sqlite_master;\n    INSERT INTO \"sqlite_stat1\" VALUES('col',NULL,'1');\n    CREATE INDEX ix_notes_usn on notes (usn);\n    CREATE INDEX ix_cards_usn on cards (usn);\n    CREATE INDEX ix_revlog_usn on revlog (usn);\n    CREATE INDEX ix_cards_nid on cards (nid);\n    CREATE INDEX ix_cards_sched on cards (did, queue, due);\n    CREATE INDEX ix_revlog_cid on revlog (cid);\n    CREATE INDEX ix_notes_csum on notes (csum);\n    COMMIT;\n  `;\n}\n"
  },
  {
    "path": "templates/template.sql",
    "content": "PRAGMA foreign_keys=OFF;\nBEGIN TRANSACTION;\nCREATE TABLE col (\n    id              integer primary key,\n    crt             integer not null,\n    mod             integer not null,\n    scm             integer not null,\n    ver             integer not null,\n    dty             integer not null,\n    usn             integer not null,\n    ls              integer not null,\n    conf            text not null,\n    models          text not null,\n    decks           text not null,\n    dconf           text not null,\n    tags            text not null\n);\nINSERT INTO \"col\" VALUES(\n  1,\n  1388548800,\n  1435645724219,\n  1435645724215,\n  11,\n  0,\n  0,\n  0,\n  '{\"nextPos\": 1, \"estTimes\": true, \"activeDecks\": [1], \"sortType\": \"noteFld\", \"timeLim\": 0, \"sortBackwards\": false, \"addToCur\": true, \"curDeck\": 1, \"newBury\": true, \"newSpread\": 0, \"dueCounts\": true, \"curModel\": \"1435645724216\", \"collapseTime\": 1200}',\n  '{\"1388596687391\": {\"vers\": [], \"name\": \"Basic-f15d2\", \"tags\": [\"Tag\"], \"did\": 1435588830424, \"usn\": -1, \"req\": [[0, \"all\", [0]]], \"flds\": [{\"name\": \"Front\", \"media\": [], \"sticky\": false, \"rtl\": false, \"ord\": 0, \"font\": \"Arial\", \"size\": 20}, {\"name\": \"Back\", \"media\": [], \"sticky\": false, \"rtl\": false, \"ord\": 1, \"font\": \"Arial\", \"size\": 20}], \"sortf\": 0, \"latexPre\": \"\\\\documentclass[12pt]{article}\\n\\\\special{papersize=3in,5in}\\n\\\\usepackage[utf8]{inputenc}\\n\\\\usepackage{amssymb,amsmath}\\n\\\\pagestyle{empty}\\n\\\\setlength{\\\\parindent}{0in}\\n\\\\begin{document}\\n\", \"tmpls\": [{\"name\": \"Card 1\", \"qfmt\": \"{{Front}}\", \"did\": null, \"bafmt\": \"\", \"afmt\": \"{{Back}}\", \"ord\": 0, \"bqfmt\": \"\"}], \"latexPost\": \"\\\\end{document}\", \"type\": 0, \"id\": 1388596687391, \"css\": \".card {\\n font-family: arial;\\n font-size: 20px;\\n text-align: center;\\n color: black;\\n background-color: white;\\n}\\n\", \"mod\": 1435645658}}',\n  '{\"1\": {\"desc\": \"\", \"name\": \"Default\", \"extendRev\": 50, \"usn\": 0, \"collapsed\": false, \"newToday\": [0, 0], \"timeToday\": [0, 0], \"dyn\": 0, \"extendNew\": 10, \"conf\": 1, \"revToday\": [0, 0], \"lrnToday\": [0, 0], \"id\": 1, \"mod\": 1435645724}, \"1435588830424\": {\"desc\": \"\", \"name\": \"Template\", \"extendRev\": 50, \"usn\": -1, \"collapsed\": false, \"newToday\": [545, 0], \"timeToday\": [545, 0], \"dyn\": 0, \"extendNew\": 10, \"conf\": 1, \"revToday\": [545, 0], \"lrnToday\": [545, 0], \"id\": 1435588830424, \"mod\": 1435588830}}',\n  '{\"1\": {\"name\": \"Default\", \"replayq\": true, \"lapse\": {\"leechFails\": 8, \"minInt\": 1, \"delays\": [10], \"leechAction\": 0, \"mult\": 0}, \"rev\": {\"perDay\": 100, \"fuzz\": 0.05, \"ivlFct\": 1, \"maxIvl\": 36500, \"ease4\": 1.3, \"bury\": true, \"minSpace\": 1}, \"timer\": 0, \"maxTaken\": 60, \"usn\": 0, \"new\": {\"perDay\": 20, \"delays\": [1, 10], \"separate\": true, \"ints\": [1, 4, 7], \"initialFactor\": 2500, \"bury\": true, \"order\": 1}, \"mod\": 0, \"id\": 1, \"autoplay\": true}}',\n  '{}'\n);\nCREATE TABLE notes (\n    id              integer primary key,   /* 0 */\n    guid            text not null,         /* 1 */\n    mid             integer not null,      /* 2 */\n    mod             integer not null,      /* 3 */\n    usn             integer not null,      /* 4 */\n    tags            text not null,         /* 5 */\n    flds            text not null,         /* 6 */\n    sfld            integer not null,      /* 7 */\n    csum            integer not null,      /* 8 */\n    flags           integer not null,      /* 9 */\n    data            text not null          /* 10 */\n);\nCREATE TABLE cards (\n    id              integer primary key,   /* 0 */\n    nid             integer not null,      /* 1 */\n    did             integer not null,      /* 2 */\n    ord             integer not null,      /* 3 */\n    mod             integer not null,      /* 4 */\n    usn             integer not null,      /* 5 */\n    type            integer not null,      /* 6 */\n    queue           integer not null,      /* 7 */\n    due             integer not null,      /* 8 */\n    ivl             integer not null,      /* 9 */\n    factor          integer not null,      /* 10 */\n    reps            integer not null,      /* 11 */\n    lapses          integer not null,      /* 12 */\n    left            integer not null,      /* 13 */\n    odue            integer not null,      /* 14 */\n    odid            integer not null,      /* 15 */\n    flags           integer not null,      /* 16 */\n    data            text not null          /* 17 */\n);\nCREATE TABLE revlog (\n    id              integer primary key,\n    cid             integer not null,\n    usn             integer not null,\n    ease            integer not null,\n    ivl             integer not null,\n    lastIvl         integer not null,\n    factor          integer not null,\n    time            integer not null,\n    type            integer not null\n);\nCREATE TABLE graves (\n    usn             integer not null,\n    oid             integer not null,\n    type            integer not null\n);\nANALYZE sqlite_master;\nINSERT INTO \"sqlite_stat1\" VALUES('col',NULL,'1');\nCREATE INDEX ix_notes_usn on notes (usn);\nCREATE INDEX ix_cards_usn on cards (usn);\nCREATE INDEX ix_revlog_usn on revlog (usn);\nCREATE INDEX ix_cards_nid on cards (nid);\nCREATE INDEX ix_cards_sched on cards (did, queue, due);\nCREATE INDEX ix_revlog_cid on revlog (cid);\nCREATE INDEX ix_notes_csum on notes (csum);\nCOMMIT;\n"
  },
  {
    "path": "test/_helpers.js",
    "content": "import fs from 'fs';\nimport path from 'path';\nimport Zip from 'jszip';\nimport mkdirp from 'mkdirp';\n\nexport const addCards = (apkg, list) => list.forEach(({ front, back }) => apkg.addCard(front, back));\n\nexport const unzipDeckToDir = (pathToDeck, pathToUnzipTo) => {\n  mkdirp.sync(pathToUnzipTo);\n  const zipContent = fs.readFileSync(pathToDeck);\n  const zip = new Zip();\n\n  return zip.loadAsync(zipContent, { createFolders: true }).then(() =>\n    Promise.all(\n      Object.keys(zip.files).map(i => {\n        const file = zip.files[i];\n        const filePath = path.join(pathToUnzipTo, file.name);\n        return file.async('nodebuffer').then(data => fs.writeFileSync(filePath, data));\n      })\n    )\n  );\n};\n"
  },
  {
    "path": "test/exporter.js",
    "content": "import test from 'ava';\nimport sinon from 'sinon';\nimport proxyquire from 'proxyquire';\nimport fs from 'fs';\nimport sql from 'sql.js';\n\nconst template = fs.readFileSync(__dirname + '/../templates/template.sql', 'utf-8');\nconst now = Date.now();\n\nconst { Exporter } = proxyquire('../src', {\n  jszip: function() {\n    this.file = () => null;\n    this.generateAsync = () => null;\n  }\n});\n\ntest.beforeEach(t => {\n  t.context.clock = sinon.useFakeTimers(now);\n\n  t.context.exporter = new Exporter('testDeckName', {\n    template,\n    sql\n  });\n});\n\ntest.afterEach(t => {\n  sinon.restore();\n  t.context.clock.restore();\n});\n\ntest('Exporter.save', t => {\n  const { exporter } = t.context;\n  const dbExportSpy = sinon.spy(exporter.db, 'export');\n  const zipFileSpy = sinon.spy(exporter.zip, 'file');\n  const zipGenerateAsyncSpy = sinon.spy(exporter.zip, 'generateAsync');\n\n  exporter.media = [{ filename: '1.jpg' }, { filename: '2.bmp' }];\n  exporter.save({ some: 'options', should: { be: 'here' } });\n\n  t.truthy(dbExportSpy.called, 'should call .export on db');\n  t.truthy(zipFileSpy.calledWithMatch('collection.anki2'), 'should save notes/cards db');\n  t.truthy(zipFileSpy.calledWithMatch('media'), 'should save media');\n  t.truthy(zipFileSpy.calledWithMatch(0), 'should save media with two files');\n  t.truthy(zipFileSpy.calledWithMatch(1), 'should save media with two files');\n  t.truthy(zipGenerateAsyncSpy.called, 'should call zip.generateAsync');\n  t.truthy(['blob', 'nodebuffer'].includes(zipGenerateAsyncSpy.args[0][0].type), 'zip generates binary file');\n});\n\ntest('Exporter.addCard', t => {\n  const { exporter } = t.context;\n\n  const { topDeckId, topModelId, separator } = exporter;\n  const [front, back] = ['Test Front', 'Test back'];\n  const exporterUpdateSpy = sinon.spy(exporter, '_update');\n\n  exporter.addCard(front, back);\n\n  t.is(exporterUpdateSpy.callCount, 2, 'should made two requests');\n\n  t.is(\n    exporterUpdateSpy.args[0][0],\n    'insert or replace into notes values(:id,:guid,:mid,:mod,:usn,:tags,:flds,:sfld,:csum,:flags,:data)'\n  );\n  const notesUpdate = exporterUpdateSpy.args[0][1];\n  t.is(notesUpdate[':sfld'], front);\n  t.is(notesUpdate[':flds'], front + separator + back);\n  t.is(notesUpdate[':mid'], topModelId);\n\n  t.is(\n    exporterUpdateSpy.args[1][0],\n    'insert or replace into cards values(:id,:nid,:did,:ord,:mod,:usn,:type,:queue,:due,:ivl,:factor,:reps,:lapses,:left,:odue,:odid,:flags,:data)'\n  );\n  const cardsUpdate = exporterUpdateSpy.args[1][1];\n  t.is(cardsUpdate[':did'], topDeckId);\n  t.is(cardsUpdate[':nid'], notesUpdate[':id'], 'should link both tables via the same note_id');\n});\n\ntest('Exporter.addCard with options (tags is array)', t => {\n  const { exporter } = t.context;\n\n  const { topModelId, separator } = exporter;\n  const [front, back] = ['Test Front', 'Test back'];\n  const tags = ['tag1', 'tag2', 'multiple words tag'];\n  const exporterUpdateSpy = sinon.spy(exporter, '_update');\n\n  exporter.addCard(front, back, { tags });\n\n  t.is(exporterUpdateSpy.callCount, 2, 'should made two requests');\n\n  t.is(\n    exporterUpdateSpy.args[0][0],\n    'insert or replace into notes values(:id,:guid,:mid,:mod,:usn,:tags,:flds,:sfld,:csum,:flags,:data)'\n  );\n  const notesUpdate = exporterUpdateSpy.args[0][1];\n  const notesTags = notesUpdate[':tags'].split(' ');\n  t.is(notesUpdate[':sfld'], front);\n  t.is(notesUpdate[':flds'], front + separator + back);\n  t.is(notesUpdate[':mid'], topModelId);\n\n  t.deepEqual(notesTags, [''].concat(tags.map(tag => tag.replace(/ /g, '_'))).concat(['']));\n});\n\ntest('Exporter.addCard with options (tags is string)', t => {\n  const { exporter } = t.context;\n  const { topDeckId, topModelId, separator } = exporter;\n  const [front, back, tags] = ['Test Front', 'Test back', 'Some string with_delimiters'];\n  const exporterUpdateSpy = sinon.spy(exporter, '_update');\n\n  exporter.addCard(front, back, { tags });\n\n  t.is(exporterUpdateSpy.callCount, 2, 'should made two requests');\n\n  t.is(\n    exporterUpdateSpy.args[0][0],\n    'insert or replace into notes values(:id,:guid,:mid,:mod,:usn,:tags,:flds,:sfld,:csum,:flags,:data)'\n  );\n  const notesUpdate = exporterUpdateSpy.args[0][1];\n  t.is(notesUpdate[':sfld'], front);\n  t.is(notesUpdate[':flds'], front + separator + back);\n  t.is(notesUpdate[':mid'], topModelId);\n  t.is(notesUpdate[':tags'], tags);\n\n  t.is(\n    exporterUpdateSpy.args[1][0],\n    'insert or replace into cards values(:id,:nid,:did,:ord,:mod,:usn,:type,:queue,:due,:ivl,:factor,:reps,:lapses,:left,:odue,:odid,:flags,:data)'\n  );\n  const cardsUpdate = exporterUpdateSpy.args[1][1];\n  t.is(cardsUpdate[':did'], topDeckId);\n  t.is(cardsUpdate[':nid'], notesUpdate[':id'], 'should link both tables via the same note_id');\n});\n\ntest('Exporter.addCard updates note if it is a duplicate', t => {\n  const { exporter } = t.context;\n\n  const { topDeckId, topModelId, separator } = exporter;\n  const [front, back] = ['Test Front', 'Test back'];\n  const exporterUpdateSpy = sinon.spy(exporter, '_update');\n\n  exporter.addCard(front, back);\n  exporter.addCard(front, back);\n\n  t.is(exporterUpdateSpy.callCount, 4, 'should made four requests');\n\n  t.is(\n    exporterUpdateSpy.args[0][0],\n    'insert or replace into notes values(:id,:guid,:mid,:mod,:usn,:tags,:flds,:sfld,:csum,:flags,:data)'\n  );\n  const notesUpdate = exporterUpdateSpy.args[0][1];\n  const secondNotesUpdate = exporterUpdateSpy.args[2][1];\n  t.is(notesUpdate[':id'], secondNotesUpdate[':id']);\n  t.is(notesUpdate[':guid'], secondNotesUpdate[':guid']);\n  t.is(notesUpdate[':sfld'], front);\n  t.is(notesUpdate[':flds'], front + separator + back);\n  t.is(notesUpdate[':mid'], topModelId);\n\n  t.is(\n    exporterUpdateSpy.args[1][0],\n    'insert or replace into cards values(:id,:nid,:did,:ord,:mod,:usn,:type,:queue,:due,:ivl,:factor,:reps,:lapses,:left,:odue,:odid,:flags,:data)'\n  );\n  const cardsUpdate = exporterUpdateSpy.args[1][1];\n  const secondCardsUpdate = exporterUpdateSpy.args[3][1];\n  t.is(cardsUpdate[':id'], secondCardsUpdate[':id']);\n  t.is(cardsUpdate[':did'], topDeckId);\n  t.is(cardsUpdate[':nid'], notesUpdate[':id'], 'should link both tables via the same note_id');\n});\n\ntest('Exporter._getId', t => {\n  const { exporter } = t.context;\n  const numberOfCards = 5;\n  const [front, back] = ['Test Front', 'Test back'];\n  for (let i = 0; i < numberOfCards; i++) {\n    exporter.addCard(`${front} ${i}`, `${back} ${i}`);\n  }\n\n  const noteIdsResult = exporter.db.exec('SELECT id FROM notes ORDER BY id DESC');\n  t.deepEqual(\n    noteIdsResult,\n    [\n      {\n        columns: ['id'],\n        values: new Array(numberOfCards)\n          .fill(0)\n          .map((el, index) => [now + index])\n          .sort((a, b) => b[0] - a[0])\n      }\n    ],\n    'It should increment values inserted at the same time'\n  );\n});\n"
  },
  {
    "path": "test/test.js",
    "content": "import test from 'ava';\n\nimport fs from 'fs';\nimport { execFile } from 'child_process';\nimport sqlite3 from 'sqlite3';\nimport isArrayBufferEqual from 'arraybuffer-equal';\n\nimport pify from 'pify';\nimport sinon from 'sinon';\nimport sortBy from 'lodash.sortby';\n\nimport AnkiExport from '../src';\nimport { addCards, unzipDeckToDir } from './_helpers';\n\nconst tmpDir = '/tmp';\nconst dest = tmpDir + '/result.apkg';\nconst destUnpacked = tmpDir + '/unpacked_result';\nconst destUnpackedDb = destUnpacked + '/collection.anki2';\nconst SEPARATOR = '\\u001F';\n\ntest.beforeEach(async () => pify(execFile)('rm', ['-rf', dest, destUnpacked]));\n\ntest('equals to sample', async t => {\n  const now = 1482680798652;\n  const clock = sinon.useFakeTimers(now);\n\n  const apkg = new AnkiExport('deck-name');\n\n  apkg.addMedia('anki.png', fs.readFileSync(__dirname + '/fixtures/anki.png'));\n\n  apkg.addCard('card #1 front', 'card #1 back', { tags: ['food', 'fruit'] });\n  apkg.addCard('card #2 front', 'card #2 back');\n  apkg.addCard('card #3 with image <img src=\"anki.png\" />', 'card #3 back');\n\n  const zip = await apkg.save();\n  fs.writeFileSync(dest, zip, 'binary');\n\n  t.true(zip instanceof Buffer);\n\n  const sampleZip = fs.readFileSync(`${__dirname}/fixtures/output.apkg`);\n  const destZip = fs.readFileSync(dest);\n  t.true(isArrayBufferEqual(destZip.buffer, sampleZip.buffer));\n\n  sinon.restore();\n  clock.restore();\n});\n\ntest('check internal structure', async t => {\n  // Create deck as in previous example\n  const apkg = new AnkiExport('deck-name');\n  const cards = [\n    { front: 'card #1 front', back: 'card #1 back' },\n    { front: 'card #2 front', back: 'card #2 back' },\n    { front: 'card #3 with image <img src=\"anki.png\" />', back: 'card #3 back' }\n  ];\n  addCards(apkg, cards);\n  const zip = await apkg.save();\n  fs.writeFileSync(dest, zip, 'binary');\n\n  // extract dec to tmp directory\n  await unzipDeckToDir(dest, destUnpacked);\n  // analize db via sqlite\n  const db = new sqlite3.Database(destUnpackedDb);\n  const result = await pify(db.all.bind(db))(\n    `SELECT\n      notes.sfld as front,\n      notes.flds as back\n      from cards JOIN notes where cards.nid = notes.id ORDER BY cards.id`\n  );\n  db.close();\n\n  // compare content from just created db with original list of cards\n  const normilizedResult = sortBy(\n    result.map(({ front, back }) => ({\n      front,\n      back: back.split(SEPARATOR).pop()\n    })),\n    'front'\n  );\n\n  t.deepEqual(normilizedResult, cards);\n});\n\ntest('check internal structure on adding card with tags', async t => {\n  const decFile = `${dest}_with_tags.apkg`;\n  const unzipedDeck = `${destUnpacked}_with_tags`;\n  const apkg = new AnkiExport('deck-name');\n  const [front1, back1, tags1] = ['Card front side 1', 'Card back side 1', ['some', 'tag', 'tags with multiple words']];\n  const [front2, back2, tags2] = ['Card front side 2', 'Card back side 2', 'some strin_tags'];\n  const [front3, back3] = ['Card front side 3', 'Card back side 3'];\n  apkg.addCard(front1, back1, { tags: tags1 });\n  apkg.addCard(front2, back2, { tags: tags2 });\n  apkg.addCard(front3, back3);\n\n  const zip = await apkg.save();\n  fs.writeFileSync(decFile, zip, 'binary');\n\n  await unzipDeckToDir(decFile, unzipedDeck);\n  const db = new sqlite3.Database(`${unzipedDeck}/collection.anki2`);\n  const results = await pify(db.all.bind(db))(\n    `SELECT\n      notes.sfld as front,\n      notes.flds as back,\n      notes.tags as tags\n      from cards JOIN notes where cards.nid = notes.id ORDER BY front`\n  );\n  db.close();\n\n  t.deepEqual(results, [\n    {\n      front: front1,\n      back: `${front1}${SEPARATOR}${back1}`,\n      tags: ' ' + tags1.map(tag => tag.replace(/ /g, '_')).join(' ') + ' '\n    },\n    { front: front2, back: `${front2}${SEPARATOR}${back2}`, tags: tags2 },\n    { front: front3, back: `${front3}${SEPARATOR}${back3}`, tags: '' }\n  ]);\n});\n"
  }
]