master 8ad8d7efab5f cached
37 files
38.9 KB
12.4k tokens
27 symbols
1 requests
Download .txt
Repository: repeat-space/anki-apkg-export
Branch: master
Commit: 8ad8d7efab5f
Files: 37
Total size: 38.9 KB

Directory structure:
gitextract_0s2q98dy/

├── .babelrc
├── .editorconfig
├── .eslintrc.json
├── .gitignore
├── .npmignore
├── .travis.yml
├── README.md
├── ava.config.js
├── examples/
│   ├── browser/
│   │   ├── .babelrc
│   │   ├── .gitignore
│   │   ├── index.html
│   │   ├── index.js
│   │   ├── package.json
│   │   └── webpack.config.js
│   ├── browser-media-ajax/
│   │   ├── .babelrc
│   │   ├── .gitignore
│   │   ├── index.html
│   │   ├── index.js
│   │   ├── package.json
│   │   └── webpack.config.js
│   ├── browser-media-file-input/
│   │   ├── .babelrc
│   │   ├── .gitignore
│   │   ├── index.html
│   │   ├── index.js
│   │   ├── package.json
│   │   └── webpack.config.js
│   └── server/
│       └── server.js
├── husky.config.js
├── package.json
├── src/
│   ├── exporter.js
│   ├── index.js
│   └── template.js
├── templates/
│   └── template.sql
└── test/
    ├── _helpers.js
    ├── exporter.js
    ├── fixtures/
    │   └── output.apkg
    └── test.js

================================================
FILE CONTENTS
================================================

================================================
FILE: .babelrc
================================================
{
  "presets": ["@babel/preset-env"]
}


================================================
FILE: .editorconfig
================================================
# http://editorconfig.org
root = true

[*]
indent_style = space
indent_size = 2
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true

[*.md]
trim_trailing_whitespace = false


================================================
FILE: .eslintrc.json
================================================
{
  "parser": "babel-eslint",
  "globals": {
    "Promise": true
  },
  "env": {
    "browser": true,
    "node": true
  },
  "extends": ["plugin:prettier/recommended"],
  "rules": {
    "arrow-parens": [
      2,
      "as-needed"
    ],
    "keyword-spacing": 2,
    "no-extra-semi": 2,
    "no-undef": 2,
    "no-unused-vars": 2,
    "no-var": 2,
    "prettier/prettier": [
      2,
      {
        "printWidth": 120,
        "singleQuote": true,
        "trailingComma": "none"
      }
    ],
    "semi": [
      2,
      "always"
    ]
  }
}


================================================
FILE: .gitignore
================================================
/node_modules/
/dist/
*.log
play.js


================================================
FILE: .npmignore
================================================
.gitignore


================================================
FILE: .travis.yml
================================================
language: node_js
node_js:
  - '12'
  - '10'


================================================
FILE: README.md
================================================
# anki-apkg-export

[![Build Status](https://travis-ci.org/repeat-space/anki-apkg-export.svg?branch=master)](https://travis-ci.org/repeat-space/anki-apkg-export)

Universal module for generating decks for Anki.

Port of the Ruby gem https://github.com/albertzak/anki2

## Install

```
$ npm install anki-apkg-export --save
```

## Usage

### server

```js
const fs = require('fs');
const AnkiExport = require('anki-apkg-export').default;

const apkg = new AnkiExport('deck-name');

apkg.addMedia('anki.png', fs.readFileSync('anki.png'));

apkg.addCard('card #1 front', 'card #1 back');
apkg.addCard('card #2 front', 'card #2 back', { tags: ['nice', 'better card'] });
apkg.addCard('card #3 with image <img src="anki.png" />', 'card #3 back');

apkg
  .save()
  .then(zip => {
    fs.writeFileSync('./output.apkg', zip, 'binary');
    console.log(`Package has been generated: output.pkg`);
  })
  .catch(err => console.log(err.stack || err));
```

### browser

Intended to be used with [`webpack`](https://github.com/webpack/webpack)

```js
const webpack = require('webpack');

module.exports = {
  entry: './index.js',
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel'
      },
    ]
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: JSON.stringify(process.env.NODE_ENV || 'development')
      },
    })
  ],
  output: {
    path: __dirname,
    filename: 'bundle.js'
  }
};
```

Required loaders:

- [`script-loader`](https://github.com/webpack/script-loader)

```js
import { saveAs } from 'file-saver';
import AnkiExport from 'anki-apkg-export';

const apkg = new AnkiExport('deck-name');

// could be a File from <input /> or a Blob from fetch
// take a look at the example folder for a complete overview
apkg.addMedia('anki.png', file);

apkg.addCard('card #1 front', 'card #1 back');
apkg.addCard('card #2 front', 'card #2 back', { tags: ['nice', 'better card'] });
apkg.addCard('card #3 with image <img src="anki.png" />', 'card #3 back');

apkg
  .save()
  .then(zip => {
    saveAs(zip, 'output.apkg');
  })
  .catch(err => console.log(err.stack || err));
```

## Examples

- [server from above](examples/server)
- [browser from above](examples/browser)
- [browser usage with media attachments via ajax](examples/browser-media-ajax)
- [browser usage with media attachments via <form />](examples/browser-media-file-input)

## Changelog

- `v4.0.0` - expose template variables (frontside, backside and css)
- `v3.1.0` - make setting APP_ENV optional
- `v3.0.0` - add tags, ES6 refactor (breaking)
- `v2.0.0` - add media support, update jszip dependency (breaking)
- `v1.0.0` - initial rewrite

## Tips

- [issue#25](https://github.com/ewnd9/anki-apkg-export/issues/25) - Dealing with `sql.js` memory limits

## Related

- [apkg format documentation](http://decks.wikia.com/wiki/Anki_APKG_format_documentation)
- [anki-apkg-export-cli](https://github.com/ewnd9/anki-apkg-export-cli) - CLI for this module
- [anki-apkg-export-app](https://github.com/ewnd9/anki-apkg-export-app) - Simple web app to generate cards online

## License

MIT © [ewnd9](http://ewnd9.com)


================================================
FILE: ava.config.js
================================================
export default {
  require: ['@babel/register', '@babel/polyfill']
};


================================================
FILE: examples/browser/.babelrc
================================================
{
  "presets": ["es2015"]
}


================================================
FILE: examples/browser/.gitignore
================================================
node_modules
bundle.js


================================================
FILE: examples/browser/index.html
================================================
<script src="/bundle.js"></script>


================================================
FILE: examples/browser/index.js
================================================
import { saveAs } from 'file-saver';

import AnkiExport from '../../src';
// import AnkiExport from 'anki-apkg-export';

const apkg = new AnkiExport('deck-name');

apkg.addCard('card #1 front', 'card #1 back');
apkg.addCard('card #2 front', 'card #2 back');

apkg
  .save()
  .then(zip => {
    saveAs(zip, 'output.apkg');
  })
  .catch(err => console.log(err.stack || err));


================================================
FILE: examples/browser/package.json
================================================
{
  "private": true,
  "scripts": {
    "build": "webpack"
  },
  "dependencies": {
    "anki-apkg-export": "^3.0.1",
    "babel-loader": "^6.2.4",
    "babel-preset-es2015": "^6.6.0",
    "file-saver": "^1.3.3"
  },
  "devDependencies": {
    "script-loader": "^0.6.1",
    "webpack": "^1.12.14"
  }
}


================================================
FILE: examples/browser/webpack.config.js
================================================
'use strict';

const webpack = require('webpack');

module.exports = {
  entry: './index.js',
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel'
      },
    ]
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: JSON.stringify(process.env.NODE_ENV || 'development'),
        APP_ENV: JSON.stringify('browser')
      },
    })
  ],
  output: {
    path: __dirname,
    filename: 'bundle.js'
  }
};


================================================
FILE: examples/browser-media-ajax/.babelrc
================================================
{
  "presets": ["es2015"]
}


================================================
FILE: examples/browser-media-ajax/.gitignore
================================================
node_modules
bundle.js


================================================
FILE: examples/browser-media-ajax/index.html
================================================
<script src="/bundle.js"></script>


================================================
FILE: examples/browser-media-ajax/index.js
================================================
import { saveAs } from 'file-saver';

import AnkiExport from '../../src';
// import AnkiExport from 'anki-apkg-export';

const apkg = new AnkiExport('deck-name-ajax');

const params = {
  method: 'GET'
};

fetch('https://raw.githubusercontent.com/ewnd9/anki-apkg-export/39ebdd664ab23b5237eee95b7dd88c457e263a20/example/assets/anki.png', params)
  .then(function(response) {
    return response.blob();
  })
  .then(function(myBlob) {
    apkg.addMedia('anki.png', myBlob);
    apkg.addCard('card #1 with image <img src="anki.png" />', 'card #1 back');

    return apkg.save()
  })
  .then(function(zip) {
    saveAs(zip, 'output.apkg');
  })
  .catch(err => console.log(err.stack || err));


================================================
FILE: examples/browser-media-ajax/package.json
================================================
{
  "private": true,
  "scripts": {
    "build": "webpack"
  },
  "dependencies": {
    "anki-apkg-export": "^3.0.1",
    "babel-loader": "^6.2.4",
    "babel-preset-es2015": "^6.6.0",
    "file-saver": "^1.3.3"
  },
  "devDependencies": {
    "script-loader": "^0.6.1",
    "webpack": "^1.12.14"
  }
}


================================================
FILE: examples/browser-media-ajax/webpack.config.js
================================================
'use strict';

const webpack = require('webpack');

module.exports = {
  entry: './index.js',
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel'
      },
    ]
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: JSON.stringify(process.env.NODE_ENV || 'development'),
        APP_ENV: JSON.stringify('browser')
      },
    })
  ],
  output: {
    path: __dirname,
    filename: 'bundle.js'
  }
};


================================================
FILE: examples/browser-media-file-input/.babelrc
================================================
{
  "presets": ["es2015"]
}


================================================
FILE: examples/browser-media-file-input/.gitignore
================================================
node_modules
bundle.js


================================================
FILE: examples/browser-media-file-input/index.html
================================================
<input id="input" type="file" />
<script src="/bundle.js"></script>


================================================
FILE: examples/browser-media-file-input/index.js
================================================
import { saveAs } from 'file-saver';

import AnkiExport from '../../src';
// import AnkiExport from 'anki-apkg-export';

const apkg = new AnkiExport('deck-name');
const input = document.getElementById('input');

input.onchange = function(e) {
  const reader = new FileReader();
  const filename = e.target.files[0].name;

  reader.onload = function(e) {
    const file = e.target.result;

    apkg.addMedia('anki.png', file);
    apkg.addCard('card #1 with image <img src="anki.png" />', 'card #1 back');

    apkg
      .save()
      .then(zip => {
        saveAs(zip, 'output.apkg');
      })
      .catch(err => console.log(err.stack || err));
  };

  reader.readAsArrayBuffer(e.target.files[0]);
};


================================================
FILE: examples/browser-media-file-input/package.json
================================================
{
  "private": true,
  "scripts": {
    "build": "webpack"
  },
  "dependencies": {
    "anki-apkg-export": "^3.0.1",
    "babel-loader": "^6.2.4",
    "babel-preset-es2015": "^6.6.0",
    "file-saver": "^1.3.3"
  },
  "devDependencies": {
    "script-loader": "^0.6.1",
    "webpack": "^1.12.14"
  }
}


================================================
FILE: examples/browser-media-file-input/webpack.config.js
================================================
'use strict';

const webpack = require('webpack');

module.exports = {
  entry: './index.js',
  module: {
    loaders: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        loader: 'babel'
      },
    ]
  },
  plugins: [
    new webpack.DefinePlugin({
      'process.env': {
        NODE_ENV: JSON.stringify(process.env.NODE_ENV || 'development'),
        APP_ENV: JSON.stringify('browser')
      },
    })
  ],
  output: {
    path: __dirname,
    filename: 'bundle.js'
  }
};


================================================
FILE: examples/server/server.js
================================================
'use strict';

const fs = require('fs');

// const { default: AnkiExport } = require('anki-apkg-export');
const { default: AnkiExport } = require('../../dist');

const apkg = new AnkiExport('deck-name-node');

apkg.addMedia('anki.png', fs.readFileSync('../assets/anki.png'));

apkg.addCard('card #1 front', 'card #1 back');
apkg.addCard('card #2 front', 'card #2 back');
apkg.addCard('card #3 with image <img src="anki.png" />', 'card #3 back');

apkg
  .save()
  .then(zip => {
    fs.writeFileSync('./output.apkg', zip, 'binary');
    console.log(`Package has been generated: output.apkg`);
  })
  .catch(err => console.log(err.stack || err));


================================================
FILE: husky.config.js
================================================
module.exports = {
  hooks: {
    'pre-commit': 'npm test'
  }
};


================================================
FILE: package.json
================================================
{
  "name": "anki-apkg-export",
  "description": "Generate decks for Anki (spaced repetition software)",
  "version": "4.0.3",
  "main": "dist/index.js",
  "dependencies": {
    "jszip": "^3.2.2",
    "sha1": "^1.1.1",
    "sql.js": "^0.5.0"
  },
  "devDependencies": {
    "@babel/cli": "^7.0.0",
    "@babel/core": "^7.0.0",
    "@babel/polyfill": "^7.0.0",
    "@babel/preset-env": "^7.0.0",
    "@babel/register": "^7.0.0",
    "arraybuffer-equal": "1.0.4",
    "ava": "^2.4.0",
    "babel-eslint": "^10.0.3",
    "eslint": "^6.8.0",
    "eslint-config-prettier": "^6.9.0",
    "eslint-plugin-prettier": "^3.1.2",
    "husky": "^3.1.0",
    "lodash.sortby": "4.7.0",
    "mkdirp": "0.5.1",
    "pify": "^4.0.1",
    "prettier": "^1.19.1",
    "proxyquire": "^2.1.3",
    "sinon": "^8.0.2",
    "sqlite3": "4.1.1"
  },
  "author": "ewnd9 <ewndnine@gmail.com>",
  "keywords": [
    "anki",
    "spaced repetition software",
    "webpack"
  ],
  "license": "MIT",
  "preferGlobal": "true",
  "repository": {
    "type": "git",
    "url": "git+https://github.com/ewnd9/anki-apkg-export.git"
  },
  "scripts": {
    "build": "babel -d dist src",
    "build:watch": "babel -w -d dist src",
    "lint": "eslint 'src/**/*.js' 'test/**/*.js'",
    "postpublish": "rm -rf dist",
    "prepare": "yarn run build",
    "test": "yarn run lint && ava",
    "test:watch": "yarn run test -- --watch"
  }
}


================================================
FILE: src/exporter.js
================================================
import sha1 from 'sha1';
import Zip from 'jszip';

export default class {
  constructor(deckName, { template, sql }) {
    this.db = new sql.Database();
    this.db.run(template);

    const now = Date.now();
    const topDeckId = this._getId('cards', 'did', now);
    const topModelId = this._getId('notes', 'mid', now);

    this.deckName = deckName;
    this.zip = new Zip();
    this.media = [];
    this.topDeckId = topDeckId;
    this.topModelId = topModelId;
    this.separator = '\u001F';

    const decks = this._getInitialRowValue('col', 'decks');
    const deck = getLastItem(decks);
    deck.name = this.deckName;
    deck.id = topDeckId;
    decks[topDeckId + ''] = deck;
    this._update('update col set decks=:decks where id=1', { ':decks': JSON.stringify(decks) });

    const models = this._getInitialRowValue('col', 'models');
    const model = getLastItem(models);
    model.name = this.deckName;
    model.did = this.topDeckId;
    model.id = topModelId;
    models[`${topModelId}`] = model;
    this._update('update col set models=:models where id=1', { ':models': JSON.stringify(models) });
  }

  save(options) {
    const { zip, db, media } = this;
    const binaryArray = db.export();
    const mediaObj = media.reduce((prev, curr, idx) => {
      prev[idx] = curr.filename;
      return prev;
    }, {});

    zip.file('collection.anki2', new Buffer(binaryArray));
    zip.file('media', JSON.stringify(mediaObj));

    media.forEach((item, i) => zip.file(i, item.data));

    if (process.env.APP_ENV === 'browser' || typeof window !== 'undefined') {
      return zip.generateAsync(Object.assign({}, { type: 'blob' }, options));
    } else {
      return zip.generateAsync(
        Object.assign(
          {},
          {
            type: 'nodebuffer',
            base64: false,
            compression: 'DEFLATE'
          },
          options
        )
      );
    }
  }

  addMedia(filename, data) {
    this.media.push({ filename, data });
  }

  addCard(front, back, { tags } = {}) {
    const { topDeckId, topModelId, separator } = this;
    const now = Date.now();
    const note_guid = this._getNoteGuid(topDeckId, front, back);
    const note_id = this._getNoteId(note_guid, now);

    let strTags = '';
    if (typeof tags === 'string') {
      strTags = tags;
    } else if (Array.isArray(tags)) {
      strTags = this._tagsToStr(tags);
    }

    this._update('insert or replace into notes values(:id,:guid,:mid,:mod,:usn,:tags,:flds,:sfld,:csum,:flags,:data)', {
      ':id': note_id, // integer primary key,
      ':guid': note_guid, // text not null,
      ':mid': topModelId, // integer not null,
      ':mod': this._getId('notes', 'mod', now), // integer not null,
      ':usn': -1, // integer not null,
      ':tags': strTags, // text not null,
      ':flds': front + separator + back, // text not null,
      ':sfld': front, // integer not null,
      ':csum': this._checksum(front + separator + back), //integer not null,
      ':flags': 0, // integer not null,
      ':data': '' // text not null,
    });

    return this._update(
      'insert or replace into cards values(:id,:nid,:did,:ord,:mod,:usn,:type,:queue,:due,:ivl,:factor,:reps,:lapses,:left,:odue,:odid,:flags,:data)',
      {
        ':id': this._getCardId(note_id, now), // integer primary key,
        ':nid': note_id, // integer not null,
        ':did': topDeckId, // integer not null,
        ':ord': 0, // integer not null,
        ':mod': this._getId('cards', 'mod', now), // integer not null,
        ':usn': -1, // integer not null,
        ':type': 0, // integer not null,
        ':queue': 0, // integer not null,
        ':due': 179, // integer not null,
        ':ivl': 0, // integer not null,
        ':factor': 0, // integer not null,
        ':reps': 0, // integer not null,
        ':lapses': 0, // integer not null,
        ':left': 0, // integer not null,
        ':odue': 0, // integer not null,
        ':odid': 0, // integer not null,
        ':flags': 0, // integer not null,
        ':data': '' // text not null
      }
    );
  }

  _update(query, obj) {
    this.db.prepare(query).getAsObject(obj);
  }

  _getInitialRowValue(table, column = 'id') {
    const query = `select ${column} from ${table}`;
    return this._getFirstVal(query);
  }

  _checksum(str) {
    return parseInt(sha1(str).substr(0, 8), 16);
  }

  _getFirstVal(query) {
    return JSON.parse(this.db.exec(query)[0].values[0]);
  }

  _tagsToStr(tags = []) {
    return ' ' + tags.map(tag => tag.replace(/ /g, '_')).join(' ') + ' ';
  }

  _getId(table, col, ts) {
    const query = `SELECT ${col} from ${table} WHERE ${col} >= :ts ORDER BY ${col} DESC LIMIT 1`;
    const rowObj = this.db.prepare(query).getAsObject({ ':ts': ts });

    return rowObj[col] ? +rowObj[col] + 1 : ts;
  }

  _getNoteId(guid, ts) {
    const query = `SELECT id from notes WHERE guid = :guid ORDER BY id DESC LIMIT 1`;
    const rowObj = this.db.prepare(query).getAsObject({ ':guid': guid });

    return rowObj.id || this._getId('notes', 'id', ts);
  }

  _getNoteGuid(topDeckId, front, back) {
    return sha1(`${topDeckId}${front}${back}`);
  }

  _getCardId(note_id, ts) {
    const query = `SELECT id from cards WHERE nid = :note_id ORDER BY id DESC LIMIT 1`;
    const rowObj = this.db.prepare(query).getAsObject({ ':note_id': note_id });

    return rowObj.id || this._getId('cards', 'id', ts);
  }
}

export const getLastItem = obj => {
  const keys = Object.keys(obj);
  const lastKey = keys[keys.length - 1];

  const item = obj[lastKey];
  delete obj[lastKey];

  return item;
};


================================================
FILE: src/index.js
================================================
import Exporter from './exporter';
import createTemplate from './template';

let sql;

if (process.env.APP_ENV === 'browser' || typeof window !== 'undefined') {
  require('script-loader!sql.js');
  sql = window.SQL;
} else {
  sql = require('sql.js');
}

export { Exporter };

export default function(deckName, template) {
  return new Exporter(deckName, {
    template: createTemplate(template),
    sql
  });
}


================================================
FILE: src/template.js
================================================
export default function createTemplate({
  questionFormat = '{{Front}}',
  answerFormat = '{{FrontSide}}\n\n<hr id="answer">\n\n{{Back}}',
  css = '.card {\n font-family: arial;\n font-size: 20px;\n text-align: center;\n color: black;\nbackground-color: white;\n}\n'
} = {}) {
  const conf = {
    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
  };

  const models = {
    1388596687391: {
      veArs: [],
      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: questionFormat,
          did: null,
          bafmt: '',
          afmt: answerFormat,
          ord: 0,
          bqfmt: ''
        }
      ],
      latexPost: '\\end{document}',
      type: 0,
      id: 1388596687391,
      css,
      mod: 1435645658
    }
  };

  const decks = {
    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
    }
  };

  const dconf = {
    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
    }
  };

  return `
    PRAGMA foreign_keys=OFF;
    BEGIN TRANSACTION;
    CREATE TABLE col (
        id              integer primary key,
        crt             integer not null,
        mod             integer not null,
        scm             integer not null,
        ver             integer not null,
        dty             integer not null,
        usn             integer not null,
        ls              integer not null,
        conf            text not null,
        models          text not null,
        decks           text not null,
        dconf           text not null,
        tags            text not null
    );
    INSERT INTO "col" VALUES(
      1,
      1388548800,
      1435645724219,
      1435645724215,
      11,
      0,
      0,
      0,
      '${JSON.stringify(conf)}',
      '${JSON.stringify(models)}',
      '${JSON.stringify(decks)}',
      '${JSON.stringify(dconf)}',
      '{}'
    );
    CREATE TABLE notes (
        id              integer primary key,   /* 0 */
        guid            text not null,         /* 1 */
        mid             integer not null,      /* 2 */
        mod             integer not null,      /* 3 */
        usn             integer not null,      /* 4 */
        tags            text not null,         /* 5 */
        flds            text not null,         /* 6 */
        sfld            integer not null,      /* 7 */
        csum            integer not null,      /* 8 */
        flags           integer not null,      /* 9 */
        data            text not null          /* 10 */
    );
    CREATE TABLE cards (
        id              integer primary key,   /* 0 */
        nid             integer not null,      /* 1 */
        did             integer not null,      /* 2 */
        ord             integer not null,      /* 3 */
        mod             integer not null,      /* 4 */
        usn             integer not null,      /* 5 */
        type            integer not null,      /* 6 */
        queue           integer not null,      /* 7 */
        due             integer not null,      /* 8 */
        ivl             integer not null,      /* 9 */
        factor          integer not null,      /* 10 */
        reps            integer not null,      /* 11 */
        lapses          integer not null,      /* 12 */
        left            integer not null,      /* 13 */
        odue            integer not null,      /* 14 */
        odid            integer not null,      /* 15 */
        flags           integer not null,      /* 16 */
        data            text not null          /* 17 */
    );
    CREATE TABLE revlog (
        id              integer primary key,
        cid             integer not null,
        usn             integer not null,
        ease            integer not null,
        ivl             integer not null,
        lastIvl         integer not null,
        factor          integer not null,
        time            integer not null,
        type            integer not null
    );
    CREATE TABLE graves (
        usn             integer not null,
        oid             integer not null,
        type            integer not null
    );
    ANALYZE sqlite_master;
    INSERT INTO "sqlite_stat1" VALUES('col',NULL,'1');
    CREATE INDEX ix_notes_usn on notes (usn);
    CREATE INDEX ix_cards_usn on cards (usn);
    CREATE INDEX ix_revlog_usn on revlog (usn);
    CREATE INDEX ix_cards_nid on cards (nid);
    CREATE INDEX ix_cards_sched on cards (did, queue, due);
    CREATE INDEX ix_revlog_cid on revlog (cid);
    CREATE INDEX ix_notes_csum on notes (csum);
    COMMIT;
  `;
}


================================================
FILE: templates/template.sql
================================================
PRAGMA foreign_keys=OFF;
BEGIN TRANSACTION;
CREATE TABLE col (
    id              integer primary key,
    crt             integer not null,
    mod             integer not null,
    scm             integer not null,
    ver             integer not null,
    dty             integer not null,
    usn             integer not null,
    ls              integer not null,
    conf            text not null,
    models          text not null,
    decks           text not null,
    dconf           text not null,
    tags            text not null
);
INSERT INTO "col" VALUES(
  1,
  1388548800,
  1435645724219,
  1435645724215,
  11,
  0,
  0,
  0,
  '{"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}',
  '{"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}}',
  '{"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}}',
  '{"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}}',
  '{}'
);
CREATE TABLE notes (
    id              integer primary key,   /* 0 */
    guid            text not null,         /* 1 */
    mid             integer not null,      /* 2 */
    mod             integer not null,      /* 3 */
    usn             integer not null,      /* 4 */
    tags            text not null,         /* 5 */
    flds            text not null,         /* 6 */
    sfld            integer not null,      /* 7 */
    csum            integer not null,      /* 8 */
    flags           integer not null,      /* 9 */
    data            text not null          /* 10 */
);
CREATE TABLE cards (
    id              integer primary key,   /* 0 */
    nid             integer not null,      /* 1 */
    did             integer not null,      /* 2 */
    ord             integer not null,      /* 3 */
    mod             integer not null,      /* 4 */
    usn             integer not null,      /* 5 */
    type            integer not null,      /* 6 */
    queue           integer not null,      /* 7 */
    due             integer not null,      /* 8 */
    ivl             integer not null,      /* 9 */
    factor          integer not null,      /* 10 */
    reps            integer not null,      /* 11 */
    lapses          integer not null,      /* 12 */
    left            integer not null,      /* 13 */
    odue            integer not null,      /* 14 */
    odid            integer not null,      /* 15 */
    flags           integer not null,      /* 16 */
    data            text not null          /* 17 */
);
CREATE TABLE revlog (
    id              integer primary key,
    cid             integer not null,
    usn             integer not null,
    ease            integer not null,
    ivl             integer not null,
    lastIvl         integer not null,
    factor          integer not null,
    time            integer not null,
    type            integer not null
);
CREATE TABLE graves (
    usn             integer not null,
    oid             integer not null,
    type            integer not null
);
ANALYZE sqlite_master;
INSERT INTO "sqlite_stat1" VALUES('col',NULL,'1');
CREATE INDEX ix_notes_usn on notes (usn);
CREATE INDEX ix_cards_usn on cards (usn);
CREATE INDEX ix_revlog_usn on revlog (usn);
CREATE INDEX ix_cards_nid on cards (nid);
CREATE INDEX ix_cards_sched on cards (did, queue, due);
CREATE INDEX ix_revlog_cid on revlog (cid);
CREATE INDEX ix_notes_csum on notes (csum);
COMMIT;


================================================
FILE: test/_helpers.js
================================================
import fs from 'fs';
import path from 'path';
import Zip from 'jszip';
import mkdirp from 'mkdirp';

export const addCards = (apkg, list) => list.forEach(({ front, back }) => apkg.addCard(front, back));

export const unzipDeckToDir = (pathToDeck, pathToUnzipTo) => {
  mkdirp.sync(pathToUnzipTo);
  const zipContent = fs.readFileSync(pathToDeck);
  const zip = new Zip();

  return zip.loadAsync(zipContent, { createFolders: true }).then(() =>
    Promise.all(
      Object.keys(zip.files).map(i => {
        const file = zip.files[i];
        const filePath = path.join(pathToUnzipTo, file.name);
        return file.async('nodebuffer').then(data => fs.writeFileSync(filePath, data));
      })
    )
  );
};


================================================
FILE: test/exporter.js
================================================
import test from 'ava';
import sinon from 'sinon';
import proxyquire from 'proxyquire';
import fs from 'fs';
import sql from 'sql.js';

const template = fs.readFileSync(__dirname + '/../templates/template.sql', 'utf-8');
const now = Date.now();

const { Exporter } = proxyquire('../src', {
  jszip: function() {
    this.file = () => null;
    this.generateAsync = () => null;
  }
});

test.beforeEach(t => {
  t.context.clock = sinon.useFakeTimers(now);

  t.context.exporter = new Exporter('testDeckName', {
    template,
    sql
  });
});

test.afterEach(t => {
  sinon.restore();
  t.context.clock.restore();
});

test('Exporter.save', t => {
  const { exporter } = t.context;
  const dbExportSpy = sinon.spy(exporter.db, 'export');
  const zipFileSpy = sinon.spy(exporter.zip, 'file');
  const zipGenerateAsyncSpy = sinon.spy(exporter.zip, 'generateAsync');

  exporter.media = [{ filename: '1.jpg' }, { filename: '2.bmp' }];
  exporter.save({ some: 'options', should: { be: 'here' } });

  t.truthy(dbExportSpy.called, 'should call .export on db');
  t.truthy(zipFileSpy.calledWithMatch('collection.anki2'), 'should save notes/cards db');
  t.truthy(zipFileSpy.calledWithMatch('media'), 'should save media');
  t.truthy(zipFileSpy.calledWithMatch(0), 'should save media with two files');
  t.truthy(zipFileSpy.calledWithMatch(1), 'should save media with two files');
  t.truthy(zipGenerateAsyncSpy.called, 'should call zip.generateAsync');
  t.truthy(['blob', 'nodebuffer'].includes(zipGenerateAsyncSpy.args[0][0].type), 'zip generates binary file');
});

test('Exporter.addCard', t => {
  const { exporter } = t.context;

  const { topDeckId, topModelId, separator } = exporter;
  const [front, back] = ['Test Front', 'Test back'];
  const exporterUpdateSpy = sinon.spy(exporter, '_update');

  exporter.addCard(front, back);

  t.is(exporterUpdateSpy.callCount, 2, 'should made two requests');

  t.is(
    exporterUpdateSpy.args[0][0],
    'insert or replace into notes values(:id,:guid,:mid,:mod,:usn,:tags,:flds,:sfld,:csum,:flags,:data)'
  );
  const notesUpdate = exporterUpdateSpy.args[0][1];
  t.is(notesUpdate[':sfld'], front);
  t.is(notesUpdate[':flds'], front + separator + back);
  t.is(notesUpdate[':mid'], topModelId);

  t.is(
    exporterUpdateSpy.args[1][0],
    'insert or replace into cards values(:id,:nid,:did,:ord,:mod,:usn,:type,:queue,:due,:ivl,:factor,:reps,:lapses,:left,:odue,:odid,:flags,:data)'
  );
  const cardsUpdate = exporterUpdateSpy.args[1][1];
  t.is(cardsUpdate[':did'], topDeckId);
  t.is(cardsUpdate[':nid'], notesUpdate[':id'], 'should link both tables via the same note_id');
});

test('Exporter.addCard with options (tags is array)', t => {
  const { exporter } = t.context;

  const { topModelId, separator } = exporter;
  const [front, back] = ['Test Front', 'Test back'];
  const tags = ['tag1', 'tag2', 'multiple words tag'];
  const exporterUpdateSpy = sinon.spy(exporter, '_update');

  exporter.addCard(front, back, { tags });

  t.is(exporterUpdateSpy.callCount, 2, 'should made two requests');

  t.is(
    exporterUpdateSpy.args[0][0],
    'insert or replace into notes values(:id,:guid,:mid,:mod,:usn,:tags,:flds,:sfld,:csum,:flags,:data)'
  );
  const notesUpdate = exporterUpdateSpy.args[0][1];
  const notesTags = notesUpdate[':tags'].split(' ');
  t.is(notesUpdate[':sfld'], front);
  t.is(notesUpdate[':flds'], front + separator + back);
  t.is(notesUpdate[':mid'], topModelId);

  t.deepEqual(notesTags, [''].concat(tags.map(tag => tag.replace(/ /g, '_'))).concat(['']));
});

test('Exporter.addCard with options (tags is string)', t => {
  const { exporter } = t.context;
  const { topDeckId, topModelId, separator } = exporter;
  const [front, back, tags] = ['Test Front', 'Test back', 'Some string with_delimiters'];
  const exporterUpdateSpy = sinon.spy(exporter, '_update');

  exporter.addCard(front, back, { tags });

  t.is(exporterUpdateSpy.callCount, 2, 'should made two requests');

  t.is(
    exporterUpdateSpy.args[0][0],
    'insert or replace into notes values(:id,:guid,:mid,:mod,:usn,:tags,:flds,:sfld,:csum,:flags,:data)'
  );
  const notesUpdate = exporterUpdateSpy.args[0][1];
  t.is(notesUpdate[':sfld'], front);
  t.is(notesUpdate[':flds'], front + separator + back);
  t.is(notesUpdate[':mid'], topModelId);
  t.is(notesUpdate[':tags'], tags);

  t.is(
    exporterUpdateSpy.args[1][0],
    'insert or replace into cards values(:id,:nid,:did,:ord,:mod,:usn,:type,:queue,:due,:ivl,:factor,:reps,:lapses,:left,:odue,:odid,:flags,:data)'
  );
  const cardsUpdate = exporterUpdateSpy.args[1][1];
  t.is(cardsUpdate[':did'], topDeckId);
  t.is(cardsUpdate[':nid'], notesUpdate[':id'], 'should link both tables via the same note_id');
});

test('Exporter.addCard updates note if it is a duplicate', t => {
  const { exporter } = t.context;

  const { topDeckId, topModelId, separator } = exporter;
  const [front, back] = ['Test Front', 'Test back'];
  const exporterUpdateSpy = sinon.spy(exporter, '_update');

  exporter.addCard(front, back);
  exporter.addCard(front, back);

  t.is(exporterUpdateSpy.callCount, 4, 'should made four requests');

  t.is(
    exporterUpdateSpy.args[0][0],
    'insert or replace into notes values(:id,:guid,:mid,:mod,:usn,:tags,:flds,:sfld,:csum,:flags,:data)'
  );
  const notesUpdate = exporterUpdateSpy.args[0][1];
  const secondNotesUpdate = exporterUpdateSpy.args[2][1];
  t.is(notesUpdate[':id'], secondNotesUpdate[':id']);
  t.is(notesUpdate[':guid'], secondNotesUpdate[':guid']);
  t.is(notesUpdate[':sfld'], front);
  t.is(notesUpdate[':flds'], front + separator + back);
  t.is(notesUpdate[':mid'], topModelId);

  t.is(
    exporterUpdateSpy.args[1][0],
    'insert or replace into cards values(:id,:nid,:did,:ord,:mod,:usn,:type,:queue,:due,:ivl,:factor,:reps,:lapses,:left,:odue,:odid,:flags,:data)'
  );
  const cardsUpdate = exporterUpdateSpy.args[1][1];
  const secondCardsUpdate = exporterUpdateSpy.args[3][1];
  t.is(cardsUpdate[':id'], secondCardsUpdate[':id']);
  t.is(cardsUpdate[':did'], topDeckId);
  t.is(cardsUpdate[':nid'], notesUpdate[':id'], 'should link both tables via the same note_id');
});

test('Exporter._getId', t => {
  const { exporter } = t.context;
  const numberOfCards = 5;
  const [front, back] = ['Test Front', 'Test back'];
  for (let i = 0; i < numberOfCards; i++) {
    exporter.addCard(`${front} ${i}`, `${back} ${i}`);
  }

  const noteIdsResult = exporter.db.exec('SELECT id FROM notes ORDER BY id DESC');
  t.deepEqual(
    noteIdsResult,
    [
      {
        columns: ['id'],
        values: new Array(numberOfCards)
          .fill(0)
          .map((el, index) => [now + index])
          .sort((a, b) => b[0] - a[0])
      }
    ],
    'It should increment values inserted at the same time'
  );
});


================================================
FILE: test/test.js
================================================
import test from 'ava';

import fs from 'fs';
import { execFile } from 'child_process';
import sqlite3 from 'sqlite3';
import isArrayBufferEqual from 'arraybuffer-equal';

import pify from 'pify';
import sinon from 'sinon';
import sortBy from 'lodash.sortby';

import AnkiExport from '../src';
import { addCards, unzipDeckToDir } from './_helpers';

const tmpDir = '/tmp';
const dest = tmpDir + '/result.apkg';
const destUnpacked = tmpDir + '/unpacked_result';
const destUnpackedDb = destUnpacked + '/collection.anki2';
const SEPARATOR = '\u001F';

test.beforeEach(async () => pify(execFile)('rm', ['-rf', dest, destUnpacked]));

test('equals to sample', async t => {
  const now = 1482680798652;
  const clock = sinon.useFakeTimers(now);

  const apkg = new AnkiExport('deck-name');

  apkg.addMedia('anki.png', fs.readFileSync(__dirname + '/fixtures/anki.png'));

  apkg.addCard('card #1 front', 'card #1 back', { tags: ['food', 'fruit'] });
  apkg.addCard('card #2 front', 'card #2 back');
  apkg.addCard('card #3 with image <img src="anki.png" />', 'card #3 back');

  const zip = await apkg.save();
  fs.writeFileSync(dest, zip, 'binary');

  t.true(zip instanceof Buffer);

  const sampleZip = fs.readFileSync(`${__dirname}/fixtures/output.apkg`);
  const destZip = fs.readFileSync(dest);
  t.true(isArrayBufferEqual(destZip.buffer, sampleZip.buffer));

  sinon.restore();
  clock.restore();
});

test('check internal structure', async t => {
  // Create deck as in previous example
  const apkg = new AnkiExport('deck-name');
  const cards = [
    { front: 'card #1 front', back: 'card #1 back' },
    { front: 'card #2 front', back: 'card #2 back' },
    { front: 'card #3 with image <img src="anki.png" />', back: 'card #3 back' }
  ];
  addCards(apkg, cards);
  const zip = await apkg.save();
  fs.writeFileSync(dest, zip, 'binary');

  // extract dec to tmp directory
  await unzipDeckToDir(dest, destUnpacked);
  // analize db via sqlite
  const db = new sqlite3.Database(destUnpackedDb);
  const result = await pify(db.all.bind(db))(
    `SELECT
      notes.sfld as front,
      notes.flds as back
      from cards JOIN notes where cards.nid = notes.id ORDER BY cards.id`
  );
  db.close();

  // compare content from just created db with original list of cards
  const normilizedResult = sortBy(
    result.map(({ front, back }) => ({
      front,
      back: back.split(SEPARATOR).pop()
    })),
    'front'
  );

  t.deepEqual(normilizedResult, cards);
});

test('check internal structure on adding card with tags', async t => {
  const decFile = `${dest}_with_tags.apkg`;
  const unzipedDeck = `${destUnpacked}_with_tags`;
  const apkg = new AnkiExport('deck-name');
  const [front1, back1, tags1] = ['Card front side 1', 'Card back side 1', ['some', 'tag', 'tags with multiple words']];
  const [front2, back2, tags2] = ['Card front side 2', 'Card back side 2', 'some strin_tags'];
  const [front3, back3] = ['Card front side 3', 'Card back side 3'];
  apkg.addCard(front1, back1, { tags: tags1 });
  apkg.addCard(front2, back2, { tags: tags2 });
  apkg.addCard(front3, back3);

  const zip = await apkg.save();
  fs.writeFileSync(decFile, zip, 'binary');

  await unzipDeckToDir(decFile, unzipedDeck);
  const db = new sqlite3.Database(`${unzipedDeck}/collection.anki2`);
  const results = await pify(db.all.bind(db))(
    `SELECT
      notes.sfld as front,
      notes.flds as back,
      notes.tags as tags
      from cards JOIN notes where cards.nid = notes.id ORDER BY front`
  );
  db.close();

  t.deepEqual(results, [
    {
      front: front1,
      back: `${front1}${SEPARATOR}${back1}`,
      tags: ' ' + tags1.map(tag => tag.replace(/ /g, '_')).join(' ') + ' '
    },
    { front: front2, back: `${front2}${SEPARATOR}${back2}`, tags: tags2 },
    { front: front3, back: `${front3}${SEPARATOR}${back3}`, tags: '' }
  ]);
});
Download .txt
gitextract_0s2q98dy/

├── .babelrc
├── .editorconfig
├── .eslintrc.json
├── .gitignore
├── .npmignore
├── .travis.yml
├── README.md
├── ava.config.js
├── examples/
│   ├── browser/
│   │   ├── .babelrc
│   │   ├── .gitignore
│   │   ├── index.html
│   │   ├── index.js
│   │   ├── package.json
│   │   └── webpack.config.js
│   ├── browser-media-ajax/
│   │   ├── .babelrc
│   │   ├── .gitignore
│   │   ├── index.html
│   │   ├── index.js
│   │   ├── package.json
│   │   └── webpack.config.js
│   ├── browser-media-file-input/
│   │   ├── .babelrc
│   │   ├── .gitignore
│   │   ├── index.html
│   │   ├── index.js
│   │   ├── package.json
│   │   └── webpack.config.js
│   └── server/
│       └── server.js
├── husky.config.js
├── package.json
├── src/
│   ├── exporter.js
│   ├── index.js
│   └── template.js
├── templates/
│   └── template.sql
└── test/
    ├── _helpers.js
    ├── exporter.js
    ├── fixtures/
    │   └── output.apkg
    └── test.js
Download .txt
SYMBOL INDEX (27 symbols across 4 files)

FILE: src/exporter.js
  method constructor (line 5) | constructor(deckName, { template, sql }) {
  method save (line 36) | save(options) {
  method addMedia (line 66) | addMedia(filename, data) {
  method addCard (line 70) | addCard(front, back, { tags } = {}) {
  method _update (line 122) | _update(query, obj) {
  method _getInitialRowValue (line 126) | _getInitialRowValue(table, column = 'id') {
  method _checksum (line 131) | _checksum(str) {
  method _getFirstVal (line 135) | _getFirstVal(query) {
  method _tagsToStr (line 139) | _tagsToStr(tags = []) {
  method _getId (line 143) | _getId(table, col, ts) {
  method _getNoteId (line 150) | _getNoteId(guid, ts) {
  method _getNoteGuid (line 157) | _getNoteGuid(topDeckId, front, back) {
  method _getCardId (line 161) | _getCardId(note_id, ts) {

FILE: src/template.js
  function createTemplate (line 1) | function createTemplate({

FILE: templates/template.sql
  type col (line 3) | CREATE TABLE col (
  type notes (line 33) | CREATE TABLE notes (
  type cards (line 46) | CREATE TABLE cards (
  type revlog (line 66) | CREATE TABLE revlog (
  type graves (line 77) | CREATE TABLE graves (
  type ix_notes_usn (line 84) | CREATE INDEX ix_notes_usn on notes (usn)
  type ix_cards_usn (line 85) | CREATE INDEX ix_cards_usn on cards (usn)
  type ix_revlog_usn (line 86) | CREATE INDEX ix_revlog_usn on revlog (usn)
  type ix_cards_nid (line 87) | CREATE INDEX ix_cards_nid on cards (nid)
  type ix_cards_sched (line 88) | CREATE INDEX ix_cards_sched on cards (did, queue, due)
  type ix_revlog_cid (line 89) | CREATE INDEX ix_revlog_cid on revlog (cid)
  type ix_notes_csum (line 90) | CREATE INDEX ix_notes_csum on notes (csum)

FILE: test/test.js
  constant SEPARATOR (line 19) | const SEPARATOR = '\u001F';
Condensed preview — 37 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (44K chars).
[
  {
    "path": ".babelrc",
    "chars": 39,
    "preview": "{\n  \"presets\": [\"@babel/preset-env\"]\n}\n"
  },
  {
    "path": ".editorconfig",
    "chars": 197,
    "preview": "# http://editorconfig.org\nroot = true\n\n[*]\nindent_style = space\nindent_size = 2\ncharset = utf-8\ntrim_trailing_whitespace"
  },
  {
    "path": ".eslintrc.json",
    "chars": 547,
    "preview": "{\n  \"parser\": \"babel-eslint\",\n  \"globals\": {\n    \"Promise\": true\n  },\n  \"env\": {\n    \"browser\": true,\n    \"node\": true\n "
  },
  {
    "path": ".gitignore",
    "chars": 36,
    "preview": "/node_modules/\n/dist/\n*.log\nplay.js\n"
  },
  {
    "path": ".npmignore",
    "chars": 11,
    "preview": ".gitignore\n"
  },
  {
    "path": ".travis.yml",
    "chars": 45,
    "preview": "language: node_js\nnode_js:\n  - '12'\n  - '10'\n"
  },
  {
    "path": "README.md",
    "chars": 3175,
    "preview": "# anki-apkg-export\n\n[![Build Status](https://travis-ci.org/repeat-space/anki-apkg-export.svg?branch=master)](https://tra"
  },
  {
    "path": "ava.config.js",
    "chars": 70,
    "preview": "export default {\n  require: ['@babel/register', '@babel/polyfill']\n};\n"
  },
  {
    "path": "examples/browser/.babelrc",
    "chars": 28,
    "preview": "{\n  \"presets\": [\"es2015\"]\n}\n"
  },
  {
    "path": "examples/browser/.gitignore",
    "chars": 23,
    "preview": "node_modules\nbundle.js\n"
  },
  {
    "path": "examples/browser/index.html",
    "chars": 35,
    "preview": "<script src=\"/bundle.js\"></script>\n"
  },
  {
    "path": "examples/browser/index.js",
    "chars": 376,
    "preview": "import { saveAs } from 'file-saver';\n\nimport AnkiExport from '../../src';\n// import AnkiExport from 'anki-apkg-export';\n"
  },
  {
    "path": "examples/browser/package.json",
    "chars": 303,
    "preview": "{\n  \"private\": true,\n  \"scripts\": {\n    \"build\": \"webpack\"\n  },\n  \"dependencies\": {\n    \"anki-apkg-export\": \"^3.0.1\",\n  "
  },
  {
    "path": "examples/browser/webpack.config.js",
    "chars": 499,
    "preview": "'use strict';\n\nconst webpack = require('webpack');\n\nmodule.exports = {\n  entry: './index.js',\n  module: {\n    loaders: ["
  },
  {
    "path": "examples/browser-media-ajax/.babelrc",
    "chars": 28,
    "preview": "{\n  \"presets\": [\"es2015\"]\n}\n"
  },
  {
    "path": "examples/browser-media-ajax/.gitignore",
    "chars": 23,
    "preview": "node_modules\nbundle.js\n"
  },
  {
    "path": "examples/browser-media-ajax/index.html",
    "chars": 35,
    "preview": "<script src=\"/bundle.js\"></script>\n"
  },
  {
    "path": "examples/browser-media-ajax/index.js",
    "chars": 690,
    "preview": "import { saveAs } from 'file-saver';\n\nimport AnkiExport from '../../src';\n// import AnkiExport from 'anki-apkg-export';\n"
  },
  {
    "path": "examples/browser-media-ajax/package.json",
    "chars": 303,
    "preview": "{\n  \"private\": true,\n  \"scripts\": {\n    \"build\": \"webpack\"\n  },\n  \"dependencies\": {\n    \"anki-apkg-export\": \"^3.0.1\",\n  "
  },
  {
    "path": "examples/browser-media-ajax/webpack.config.js",
    "chars": 499,
    "preview": "'use strict';\n\nconst webpack = require('webpack');\n\nmodule.exports = {\n  entry: './index.js',\n  module: {\n    loaders: ["
  },
  {
    "path": "examples/browser-media-file-input/.babelrc",
    "chars": 28,
    "preview": "{\n  \"presets\": [\"es2015\"]\n}\n"
  },
  {
    "path": "examples/browser-media-file-input/.gitignore",
    "chars": 23,
    "preview": "node_modules\nbundle.js\n"
  },
  {
    "path": "examples/browser-media-file-input/index.html",
    "chars": 68,
    "preview": "<input id=\"input\" type=\"file\" />\n<script src=\"/bundle.js\"></script>\n"
  },
  {
    "path": "examples/browser-media-file-input/index.js",
    "chars": 703,
    "preview": "import { saveAs } from 'file-saver';\n\nimport AnkiExport from '../../src';\n// import AnkiExport from 'anki-apkg-export';\n"
  },
  {
    "path": "examples/browser-media-file-input/package.json",
    "chars": 303,
    "preview": "{\n  \"private\": true,\n  \"scripts\": {\n    \"build\": \"webpack\"\n  },\n  \"dependencies\": {\n    \"anki-apkg-export\": \"^3.0.1\",\n  "
  },
  {
    "path": "examples/browser-media-file-input/webpack.config.js",
    "chars": 499,
    "preview": "'use strict';\n\nconst webpack = require('webpack');\n\nmodule.exports = {\n  entry: './index.js',\n  module: {\n    loaders: ["
  },
  {
    "path": "examples/server/server.js",
    "chars": 646,
    "preview": "'use strict';\n\nconst fs = require('fs');\n\n// const { default: AnkiExport } = require('anki-apkg-export');\nconst { defaul"
  },
  {
    "path": "husky.config.js",
    "chars": 66,
    "preview": "module.exports = {\n  hooks: {\n    'pre-commit': 'npm test'\n  }\n};\n"
  },
  {
    "path": "package.json",
    "chars": 1393,
    "preview": "{\n  \"name\": \"anki-apkg-export\",\n  \"description\": \"Generate decks for Anki (spaced repetition software)\",\n  \"version\": \"4"
  },
  {
    "path": "src/exporter.js",
    "chars": 5580,
    "preview": "import sha1 from 'sha1';\nimport Zip from 'jszip';\n\nexport default class {\n  constructor(deckName, { template, sql }) {\n "
  },
  {
    "path": "src/index.js",
    "chars": 413,
    "preview": "import Exporter from './exporter';\nimport createTemplate from './template';\n\nlet sql;\n\nif (process.env.APP_ENV === 'brow"
  },
  {
    "path": "src/template.js",
    "chars": 6595,
    "preview": "export default function createTemplate({\n  questionFormat = '{{Front}}',\n  answerFormat = '{{FrontSide}}\\n\\n<hr id=\"answ"
  },
  {
    "path": "templates/template.sql",
    "chars": 5206,
    "preview": "PRAGMA foreign_keys=OFF;\nBEGIN TRANSACTION;\nCREATE TABLE col (\n    id              integer primary key,\n    crt         "
  },
  {
    "path": "test/_helpers.js",
    "chars": 709,
    "preview": "import fs from 'fs';\nimport path from 'path';\nimport Zip from 'jszip';\nimport mkdirp from 'mkdirp';\n\nexport const addCar"
  },
  {
    "path": "test/exporter.js",
    "chars": 6777,
    "preview": "import test from 'ava';\nimport sinon from 'sinon';\nimport proxyquire from 'proxyquire';\nimport fs from 'fs';\nimport sql "
  },
  {
    "path": "test/test.js",
    "chars": 3854,
    "preview": "import test from 'ava';\n\nimport fs from 'fs';\nimport { execFile } from 'child_process';\nimport sqlite3 from 'sqlite3';\ni"
  }
]

// ... and 1 more files (download for full content)

About this extraction

This page contains the full source code of the repeat-space/anki-apkg-export GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 37 files (38.9 KB), approximately 12.4k tokens, and a symbol index with 27 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!