PageRenderTime 56ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 1ms

/plugins/jarvisrc.js

https://github.com/broofa/jarvis
JavaScript | 171 lines | 82 code | 18 blank | 71 comment | 19 complexity | 2b58d336d19def7721f98a4ce2d75bff MD5 | raw file
  1. /*
  2. Jarvis BOOKMARKS file
  3. - Must be valid JSON
  4. - Key names are matched in Jarvis
  5. - Values are either Bookmark definitions (objects) or aliases (strings)
  6. BOOKMARKS
  7. Bookmarks are defined as key-value pairs in a JSON object. The key is the
  8. jarvis phrase the user enters, the value is either a bookmark object, or an
  9. alias string, that refers to another key in the bookmarks file.
  10. For example, a basic bookmark:
  11. npr: {
  12. doc: 'Go to the NPR web site'
  13. url: 'http://npr.org',
  14. }
  15. A bookmark, with passed arguments as a query string:
  16. npr_search: {
  17. doc: 'Search the NPR web site',
  18. url: 'http://www.npr.org/search/index.php?searchinput={QUERY}'
  19. }
  20. CONSTANTS
  21. Jarvis ignores any keys that don't resolve to a bookmark command. However they
  22. are available for substitution in urls.
  23. NO ARGS .VS. ARGS
  24. It's not uncommon to want different behavior for a bookmark based on whether or
  25. not the user provides a query string. In these cases, use the 'url' field for
  26. the no-query case, and the 'query' field for the query case.
  27. TOKEN SUBSTITUTION
  28. Tokens are substituted recursively, until all tokens have been resolved.
  29. ALIASES
  30. Aliases are simple string values that specify which command to run, and may
  31. include the same substitution tokens as the 'url' or 'query' bookmark fields.
  32. Putting all of the above together, we get the following:
  33. NPR_ROOT: 'http://www.npr.org',
  34. npr: {
  35. doc: 'Go to (or search) the NPR web site'
  36. url: '{NPR_ROOT}',
  37. query: '{NPR_ROOT}/search/index.php?searchinput={QUERY}'
  38. },
  39. public_radio: 'npr {ARGS}',
  40. */
  41. var fs = require('fs');
  42. var path = require('path');
  43. var _ = require('underscore');
  44. commands.info = {
  45. name: 'Bookmarks',
  46. doc: 'Bookmarks defined in ~/.jarvisrc',
  47. author: 'Jarvis'
  48. };
  49. // Build commands for the search sites in the OPENSEARCH dir
  50. var RC_DIR = path.join(process.env.HOME, '.jarvisrc');
  51. var files = fs.readdirSync(RC_DIR);
  52. // List of sections we lookup keys in. The placeholder object @i=0 is
  53. // populated (later) with dynamic keys that resolve to ARGS or QUERY
  54. var sections = [{}];
  55. /**
  56. * Find the first section containing the designated key, with preference given
  57. * to the section, if specified
  58. */
  59. function sectionLookup(key, section) {
  60. if (section && (key in section)) {
  61. return section;
  62. }
  63. for (var i = 0; i < sections.length; i++) {
  64. var section = sections[i];
  65. if (key in section) {
  66. return section;
  67. }
  68. }
  69. return null;
  70. }
  71. // Load each bookmark file as a "section"
  72. files.forEach(function(file) {
  73. if (/\.json$/.test(file)) {
  74. var filepath = path.join(RC_DIR, file);
  75. console.log('Loading bookmarks: ' + filepath);
  76. var json = fs.readFileSync(filepath, 'utf8');
  77. section = JSON.parse(json);
  78. section.__memo = {}; // resolve() memoization cache
  79. sections.push(section);
  80. }
  81. });
  82. // Flag for detecting circular resolution loops
  83. var RESOLVING = {};
  84. /**
  85. * Recursive token replacement using
  86. */
  87. function resolve(val, section, crumbs) {
  88. crumbs = crumbs || [];
  89. var cl = crumbs.length;
  90. return val.replace(/\{([\w$]+)\}/g, function(match) {
  91. var token = RegExp.$1;
  92. // Find section for the token
  93. var tokenSection = sectionLookup(token, section);
  94. var tokenVal = null;
  95. if (tokenSection) {
  96. tokenVal = tokenSection.__memo[token];
  97. if (tokenVal === RESOLVING) {
  98. throw new Error('Circular token loop detected: ' + crumbs.join(' > '));
  99. } else if (tokenVal == null) {
  100. tokenVal = tokenSection[token];
  101. /// Value not in cache - resolve it
  102. tokenSection.__memo[token] = RESOLVING;
  103. crumbs[cl] = token;
  104. tokenVal = resolve(tokenVal, tokenSection, crumbs);
  105. crumbs.length = cl;
  106. tokenSection.__memo[token] = tokenVal;
  107. }
  108. }
  109. // Substitute the value we foun, or put the the token back
  110. return tokenVal || '{' + token + '}';
  111. });
  112. }
  113. function makeAction(options) {
  114. return function(query) {
  115. var template = options.query || options.url || options;
  116. if (typeof(template) != 'string') {
  117. throw new Error(options + ' does not provide an action template');
  118. }
  119. var url = resolve(template).
  120. replace(/{QUERY}/g, escape(query)).
  121. replace(/{ARGS}/g, query);
  122. return url;
  123. };
  124. }
  125. sections.forEach(function(section) {
  126. for (var key in section) {
  127. var val = resolve(key, section);
  128. var options = section[key];
  129. if (options && options.url) {
  130. commands.add({
  131. phrase: key,
  132. doc: options.doc,
  133. action: makeAction(options)
  134. });
  135. }
  136. }
  137. });