terminal.tag 6.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. /**
  2. * # Terminal Riot Tag
  3. *
  4. * Provides a pretend commandline interface, capable of displaying output from a
  5. * "shell" javascript class. Multiple terminal tags can be used on a page, using
  6. * the same shell class or different ones - each terminal will work
  7. * independently.
  8. *
  9. * ## Defaults
  10. *
  11. * The default prompt is `'$'`. There is no default the welcome message. The
  12. * default shell performs no actions: pressing enter will simply move the cursor
  13. * to the next prompt line.
  14. *
  15. * ## Usage
  16. *
  17. * <terminal shell='jsclass' welcome='text' prompt='text'></terminal>
  18. *
  19. * <script src='riot+compiler.min.js'></script>
  20. * <script src='he.js'></script>
  21. * <script src='shell.js'></script>
  22. * <script src='terminal.tag' type='riot/tag'></script>
  23. * <script>riot.mount('terminal')</script>
  24. *
  25. * ## Dependencies
  26. *
  27. * - riot.js (http://riotjs.com/)
  28. * - he.js (https://github.com/mathiasbynens/he) for HTML Entity conversion.
  29. *
  30. * ## Making A Shell Class
  31. *
  32. * The shell class must be defined before the tag is mounted. Shells can keep
  33. * prompt and welcome message settings, and must define a `run()` function.
  34. * Here is the structure of a minimal shell that does nothing:
  35. *
  36. * // Contents of myshell.js
  37. * function() myshell() {
  38. * this.prompt = ''
  39. * this.welcome = ''
  40. * this.run = function(input, terminal) {
  41. * return ''
  42. * }
  43. * }
  44. *
  45. * // Shell class used by Terminal Riot Tag in index.html
  46. * <terminal shell='myshell'></terminal>
  47. *
  48. * The `run()` function can take 2 parameters. The first will contain user input
  49. * from the command line. The second will contain the terminal object itself -
  50. * this will allow the shell access to all of the terminal's functions, mainly
  51. * through its 2 child tags, `terminal.tags.display` and
  52. * `terminal.tags.commandline`. Refer those tags below for capabilities.
  53. *
  54. * A shell can, at its most basic operation, return a string of text or HTML,
  55. * and this will be appended to the display.
  56. *
  57. * ## Apps
  58. *
  59. * A shell can also return an object that will temporarily process all input,
  60. * until the object returns control back to the shell. This can be thought of as
  61. * the shell returning an "app" object.
  62. *
  63. * An "app" object should implement a `run()` function, just like the shell
  64. * class. It should also have exit conditions under which it will return control
  65. * to the shell by calling `terminal.returnToShell()`.
  66. *
  67. * {
  68. * 'run': function(input, terminal) {
  69. * terminal.tags.commandline.hidePrompt()
  70. * terminal.tags.display.add('Hello! inputs will now come to this app.')
  71. * if (input == 'exit') {
  72. * terminal.tags.display.add('Bye! Returning control to the shell.')
  73. * terminal.tags.commandline.showPrompt()
  74. * terminal.returnToShell()
  75. * }
  76. * }
  77. * }
  78. *
  79. * An "app" is an object, so can keep state information for a current run with
  80. * `this`. For more persistent saving of information, it is also possible to
  81. * save data to `terminal` or to `terminal.shell`.
  82. */
  83. <terminal>
  84. <display welcome={ welcome } />
  85. <commandline prompt={ prompt } />
  86. // Create a new shell with the class name given to the terminal tag.
  87. var shell = { 'run': function() { return ''; } }
  88. this.shell = window[opts.shell] ? new window[opts.shell] : shell
  89. this.active = this.shell
  90. this.welcome = this.shell.welcome || opts.welcome
  91. this.prompt = this.shell.prompt || opts.prompt
  92. /**
  93. * How to process a command:
  94. *
  95. * - Make the input safe by transforming html entities.
  96. * - Keep the last command line on display before other output.
  97. * - Based on shell response, append output, or give control to an app object.
  98. * - Empty the command line of input.
  99. * - Update the display.
  100. */
  101. process(prompt, input) {
  102. input = he.encode(input)
  103. this.tags.display.add(prompt + input + '\n')
  104. var response = this.active.run(input, this)
  105. if (typeof response === 'string') {
  106. this.tags.display.add(response)
  107. }
  108. if (typeof response === 'object' && typeof response.run === 'function') {
  109. this.active = response
  110. response.run(input, this)
  111. }
  112. this.tags.commandline.command.value = ''
  113. this.update()
  114. }
  115. /**
  116. * Make the shell the active process.
  117. *
  118. * For app objects to return control to the shell when finished.
  119. */
  120. returnToShell() {
  121. this.active = this.shell
  122. }
  123. </terminal>
  124. <commandline>
  125. <form autocomplete='off' onsubmit={ process }>
  126. <raw name='lhs' content={ prompt } show={ visible } />
  127. <input type='text' name='command' />
  128. </form>
  129. <style>
  130. input[name='command'] {
  131. background: transparent;
  132. border: none; outline: none;
  133. padding: 0; margin: 0;
  134. width: 90%;
  135. }
  136. </style>
  137. this.prompt = opts.prompt || '$ '
  138. this.visible = true
  139. this.on('mount', function() {
  140. document.getElementsByName('command')[0].focus()
  141. })
  142. process() {
  143. var prompt = this.visible ? this.prompt : '';
  144. this.parent.process(prompt, this.command.value)
  145. }
  146. /**
  147. * Set the prompt value.
  148. *
  149. * Note about `lhs.write()` check:
  150. * Terminal tag setup fires `setPrompt()` before `lhs.write()` exists.
  151. * At that point, `lhs` is initialised with just its `content` attribute.
  152. * After that, `lhs.write()` is called to make all subsequent prompt changes.
  153. */
  154. setPrompt(value) {
  155. if (typeof this.tags.lhs.write !== 'undefined') {
  156. this.tags.lhs.write(value)
  157. }
  158. this.prompt = value
  159. }
  160. hidePrompt() {
  161. this.visible = false
  162. }
  163. showPrompt() {
  164. this.visible = true
  165. }
  166. </commandline>
  167. <display>
  168. <div each={ output }>
  169. <raw content={ content } />
  170. </div>
  171. add(text) {
  172. if (text) {
  173. text = this.preserveWhiteSpace(text)
  174. this.output.push({ 'content': text })
  175. }
  176. }
  177. set(text) {
  178. if (text) {
  179. this.clear()
  180. this.add(text)
  181. }
  182. }
  183. /**
  184. * Empty the display.
  185. *
  186. * An extra update is needed for subsequent changes to `output` (e.g. `add()`,
  187. * `restore()`) to display properly.
  188. */
  189. clear() {
  190. this.output.length = 0
  191. this.update()
  192. }
  193. hide() {
  194. this.saved = this.output.splice(0, this.output.length)
  195. this.clear()
  196. }
  197. restore() {
  198. if (this.saved.length > 0) {
  199. this.clear()
  200. this.output = this.saved.splice(0, this.saved.length)
  201. }
  202. }
  203. /**
  204. * Make sure whitespace displays correctly.
  205. *
  206. * Keep the following:
  207. *
  208. * - Newlines.
  209. * - Whitespace for display. This means all spaces that are not within tags.
  210. */
  211. preserveWhiteSpace(text) {
  212. text = text.replace(/(?:\r\n|\r|\n)/g, '<br />')
  213. // Search for tags or whitespace. Replace whitespace, leave the tags.
  214. text = text.replace(/<[^<]+>|( )/g, function(match, group1) {
  215. if (group1 == " ") { return '&nbsp;' }
  216. return match
  217. })
  218. return text
  219. }
  220. this.output = []
  221. this.add(opts.welcome)
  222. </display>
  223. <raw>
  224. <span></span>
  225. // Initialise contents from tag options, but expose `write()` for updating.
  226. write(text) {
  227. this.root.innerHTML = text
  228. }
  229. this.write(opts.content)
  230. </raw>