diff --git a/asl_rulebook2/bin/qa-helper/READ-ME.txt b/asl_rulebook2/bin/qa-helper/READ-ME.txt
new file mode 100644
index 0000000..39034cc
--- /dev/null
+++ b/asl_rulebook2/bin/qa-helper/READ-ME.txt
@@ -0,0 +1,12 @@
+This tool helps generate the JSON for Q+A entries. It's rough-and-ready, but it helps get the job done.
+
+Note that " ...
" can be used as a shortcut for " ... ", and there is CSS in the main program for this.
+
+Keyboard shortcuts:
+ ^p =
+ ^i = wrap selected text in
+ ^e = wrap selected text in " ) ;
+ evt.preventDefault() ;
+ } else if ( evt.ctrlKey && evt.key == "i" ) {
+ var selText = $(this).val().substring( $(this)[0].selectionStart, $(this)[0].selectionEnd ) ;
+ addText( $(this), ""+selText+"" ) ;
+ evt.preventDefault() ;
+ } else if ( evt.ctrlKey && evt.key == "e" ) {
+ var selText = $(this).val().substring( $(this)[0].selectionStart, $(this)[0].selectionEnd ) ;
+ addText( $(this), "
+
+Keyboard shortcuts (answers only):
+ ^X = Yes.
+ ^c = No.
diff --git a/asl_rulebook2/bin/qa-helper/global.css b/asl_rulebook2/bin/qa-helper/global.css
new file mode 100644
index 0000000..00f691a
--- /dev/null
+++ b/asl_rulebook2/bin/qa-helper/global.css
@@ -0,0 +1,30 @@
+body {
+ position: absolute ; top: 0 ; left: 0 ; bottom: 0 ; right: 0 ;
+ overflow: hidden ;
+ display: flex ; background: #f0f0f0 ;
+}
+ul, ol { margin-top: 0.25em ; }
+.quote { font-style: italic ; color: #666 ; }
+
+#left { flex: 1 ; margin-right: 1em ; }
+#qa { margin-top: 0.5em ; overflow: auto ; background: white ; }
+#right { display: flex ; flex-direction: column ; flex: 1 ; }
+#html-preview { flex: 1 ; }
+#json-preview { height: 15em ; min-height: 15em ; }
+
+#qa { border: 1px solid #444 ; }
+#qa .rules { height: 1.8em ; padding: 2px 5px ; border: 1px solid #666 ; }
+#qa .qa { margin: 0.5em ; padding: 0.5em ; border: 1px dotted #666 ; background: #ffffe0 ; }
+#qa .qa textarea { display: block ; width: calc(100% - 1em) ; resize: vertical ; min-height: 3em ; padding: 2px 5px ; }
+#qa .qa textarea.question { height: 8em ; }
+
+#html-preview { border: 1px solid #444 ; margin-bottom: 1em ; padding: 0.5em ; overflow: auto ; background: white ; }
+#html-preview .question { clear: both ; }
+#html-preview .answer { clear: both ; margin-top: 0.5em ; }
+#html-preview img { float: left ; margin: 0 0.5em 0.5em 0 ; }
+
+#json-preview { border: 1px solid #444 ; padding: 0.5em ; overflow: auto ; background: white ; }
+#json-preview { font-family: monospace ; white-space: pre-wrap ; }
+
+button#copy-json { position: absolute ; bottom: 5px ; right: 5px ; z-index: 10 ; }
+button#add-qa { margin-left: 0.5em ; padding: 0 ; }
diff --git a/asl_rulebook2/bin/qa-helper/index.html b/asl_rulebook2/bin/qa-helper/index.html
new file mode 100644
index 0000000..ee64b3a
--- /dev/null
+++ b/asl_rulebook2/bin/qa-helper/index.html
@@ -0,0 +1,36 @@
+
+
+
+
+
"+selText+"
" ) ;
+ evt.preventDefault() ;
+ }
+ } ) ;
+ $( "#qa" ).append( $qa ) ;
+ resizeQA() ;
+ updatePreviews() ;
+ $qa.find( ".question" ).focus() ;
+}
+
+function resizeQA()
+{
+ var $jsonPreview = $( "#json-preview" ) ;
+ var yBottom = $jsonPreview.position().top + $jsonPreview.height() ;
+ var $qa = $( "#qa" ) ;
+ var newHeight = yBottom - $qa.position().top + 4 ;
+ $qa.height( newHeight ) ;
+}
+
+// --------------------------------------------------------------------
+
+function updatePreviews()
+{
+ function extractRuleIds( val ) {
+ var ruleids = [] ;
+ val = val.replace( /&/g, "," ) ;
+ for ( var ruleid of val.split( "," ) ) {
+ ruleid = ruleid.trim() ;
+ if ( ! ruleid.match( /^[A-Z][0-9.]+$/ ) )
+ continue ;
+ ruleids.push( '"' + safeJson(ruleid) + '"' ) ;
+ }
+ return ruleids ;
+ }
+
+ function cleanContent( val ) {
+ val = val.replace( /“/g, '"' ).replace( /”/g, '"' ) ;
+ val = val.replace( /‘/g, "'" ).replace( /’/g, "'" ) ;
+ val = val.replace( /`/g, "'" ) ;
+ val = val.replace( /–/g, "-" ) ;
+ val = val.replace( />=/g, "≥" ).replace( /<=/g, "≤" ) ;
+ val = val.replace( /5\/8"/g, "⅝\"" ).replace( /1\/2"/g, "½\"" ) ;
+ val = val.replace( //g, "" ).replace( /<\/q>/g, "" ) ;
+ val = val.replace( /\n/g, " " ) ;
+ val = val.replace( /\s+/g, " " ) ;
+ return val.trim() ;
+ }
+
+ function safeJson( val ) {
+ return val.replace( /"/g, '\\"' ) ;
+ }
+
+ // process each Q+A
+ var htmlBuf=[], jsonContent=[] ;
+ var caption = $( "input.rules" ).val() ;
+ $( ".qa" ).each( function() {
+ var question = cleanContent( $(this).children( ".question" ).val() ) ;
+ var answer = cleanContent( $(this).children( ".answer" ).val() ) ;
+ // update the HTML preview
+ htmlBuf.push( "
", question, "
", answer, "