terminal.tag 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180
  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 `process()` 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.process = function(input) {
  41. * return ''
  42. * }
  43. * }
  44. *
  45. * // shell class used by Terminal Riot Tag in index.html
  46. * <terminal shell='myshell'></terminal>
  47. *
  48. * ## Special Shell Return Values
  49. *
  50. * The terminal tag recognises some predefined shell return values and upon
  51. * receiving them, can perform actions other than printing output:
  52. *
  53. * - `'clear'`. Terminal will clear its output history.
  54. *
  55. */
  56. <terminal>
  57. <history welcome={ welcome } />
  58. <commandline prompt={ prompt } />
  59. // Take defaults from shell, otherwise take from terminal tag options.
  60. var shell = { 'process': function() { return ''; } }
  61. this.shell = window[opts.shell] ? new window[opts.shell] : shell
  62. this.welcome = this.shell.welcome ? this.shell.welcome : opts.welcome
  63. this.prompt = this.shell.prompt ? this.shell.prompt : opts.prompt
  64. /**
  65. * How to process a command:
  66. * - Make the input safe by transforming html entities.
  67. * - Keep the last command in history as we go to the next line.
  68. * - Based on shell response, perform special actions or append the output.
  69. * - Update the prompt as required.
  70. */
  71. process(prompt, input) {
  72. input = he.encode(input)
  73. var output = prompt + input + '\n'
  74. var response = this.shell.process(input)
  75. switch(response) {
  76. case 'clear':
  77. this.clear()
  78. break
  79. default:
  80. this.tags.history.add(output + response)
  81. }
  82. this.tags.commandline.setPrompt(this.shell.prompt)
  83. this.tags.commandline.command.value = ''
  84. }
  85. clear() {
  86. this.tags.history.hist = []
  87. this.update()
  88. }
  89. </terminal>
  90. <commandline>
  91. <form autocomplete='off' onsubmit={ process }>
  92. <raw name='lhs' content={ prompt } /><input type='text' name='command' />
  93. </form>
  94. <style>
  95. input[name='command'] {
  96. background: transparent;
  97. border: none; outline: none;
  98. padding: 0; margin: 0;
  99. width: 90%;
  100. }
  101. </style>
  102. this.on('mount', function() {
  103. document.getElementsByName('command')[0].focus()
  104. })
  105. /**
  106. * Set the prompt to any truthy value.
  107. *
  108. * Note about `lhs.write()` check:
  109. * Terminal tag setup fires `setPrompt()` before `lhs.write()` exists.
  110. * At that point, `lhs` is initialised with just its `content` attribute.
  111. * After that, `lhs.write()` is called to make all subsequent prompt changes.
  112. */
  113. setPrompt(value) {
  114. if (value) {
  115. if (typeof this.tags.lhs.write != 'undefined') {
  116. this.tags.lhs.write(value)
  117. }
  118. this.update({ 'prompt': value })
  119. }
  120. }
  121. process(e) {
  122. this.parent.process(this.prompt, this.command.value)
  123. }
  124. this.prompt = '$ '
  125. this.setPrompt(opts.prompt)
  126. </commandline>
  127. <history>
  128. <div each={ hist }>
  129. <raw content={ content } />
  130. </div>
  131. add(output) {
  132. if (output) {
  133. output = this.preserveWhiteSpace(output)
  134. this.hist.push({ 'content': output })
  135. this.update()
  136. }
  137. }
  138. /**
  139. * Make sure whitespace displays correctly.
  140. *
  141. * Keep the following:
  142. *
  143. * - Newlines.
  144. * - Whitespace for display. This means all spaces that are not within tags.
  145. */
  146. preserveWhiteSpace(text) {
  147. text = text.replace(/(?:\r\n|\r|\n)/g, '<br />')
  148. // Search for tags or whitespace. Replace whitespace, leave the tags.
  149. text = text.replace(/<[^<]+>|( )/g, function(match, group1) {
  150. if (group1 == " ") { return '&nbsp;' }
  151. return match
  152. })
  153. return text
  154. }
  155. this.hist = []
  156. this.add(opts.welcome)
  157. </history>
  158. <raw>
  159. <span></span>
  160. // Initialise contents from tag options, but expose `write()` for updating.
  161. write(value) {
  162. this.root.innerHTML = value
  163. }
  164. this.write(opts.content)
  165. </raw>