<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE muclient>

<muclient>
<plugin
   name="MUSHclient_Help"
   author="Nick Gammon"
   id="d2fa45d390d935d947cdc169"
   language="Lua"
   purpose="Shows the MUSHclient help file"
   save_state="y"
   date_written="2011-06-13 08:58:30"
   date_modified="2016-07-17 12:25"
   requires="4.76"
   version="1.1"
   >
<description trim="y">
<![CDATA[
To get general help:

  mchelp  <search string>
  mchelps <search string>   --> get help with snippet (context) shown
  mchelp  <number>  --> show help on numbered item from previous search

  mchelp   --> on its own, shows this help

Search strings can have boolean operators in them, namely the words:

  AND NEAR NOT OR

These words must be in all capitals or they are just searched for literally.

For example:

  mchelp trigger AND alias
  mchelp delete AND trigger NEAR count
  mchelp search NOT table
  mchelp search -table
  mchelp add OR delete
  mchelp next NEAR/5 table
  mchelp add AND (trigger OR alias)

(The syntax NEAR/5 means if one word is within 5 words of another word, the default is 10)

You can use a wildcard suffix, eg.

  mchelp trigg*

You can look for phrases by putting them in quotes, eg.

  mchelp "hash a string"

You can restrict the lookup to the item name (eg. the function name) like this:

  mchelp name:utils.*

]]>
</description>

</plugin>


<!--  Aliases  -->

<aliases>
  <alias
   script="help_find"
   match="^mchelp (.+)$"
   enabled="y"
   group="help"
   regexp="y"
   sequence="100"
  >
  </alias>

  <alias
   script="help_number"
   match="^mchelp (\d+)$"
   enabled="y"
   group="help"
   regexp="y"
   sequence="90"
  >
  </alias>

 <alias
   script="help_find_snippet"
   match="^mchelps (.+)$"
   enabled="y"
   group="help"
   regexp="y"
   sequence="100"
  >
  </alias>

  <alias
   script="OnHelp"
   match="^mchelps?$"
   enabled="y"
   regexp="y"
  >
  </alias>

</aliases>

<!--  Script  -->


<script>
<![CDATA[

require "pairsbykeys"

-- major headings
local HEADINGCOLOUR = "yellow"
local NAMECOLOUR = "cyan"
local SUMMARYCOLOUR = "white"
local PROTOTYPECOLOUR = "cyan"
local DESCRIPTIONCOLOUR = "white"
local NOTESCOLOUR = "white"
local HELPCOLOUR = "indianred"

-- tags (eg. bold, italic)
local CODECOLOUR = "greenyellow"
local RETURNVALUE = "thistle"
local BOLDCOLOUR = "orange"
local ITALICCOLOUR = "linen"

-- for snippets
local RESET   = ANSI (0)
local BOLD    = ANSI (1)
local UNBOLD  = ANSI (22)
local SNIPPETSIZE = -15   -- how many tokens to show around snippet

local styles

-- SQL fixup function

local function fixsql (s)
  -- replace single quotes with two lots of single quotes
  if s then
    return "'" .. (string.gsub (s, "'", "''")) .. "'"
  end -- if string exists

  return "NULL"
end -- fixsql

-- for fixing up entities
local entities = {
  ["&lt;"]    = "<";
  ["&gt;"]    = ">";
  ["&amp;"]   = "&";
  ["&quot;"]  = "\"";
  } -- end of entities

-- what to do on getting a tag like <b>
local tag_handlers = {
  ["<b>"]     = function () SetNoteColourFore (ColourNameToRGB (BOLDCOLOUR))    end;
  ["<i>"]     = function () SetNoteColourFore (ColourNameToRGB (ITALICCOLOUR))  end;
  ["<code>"]  = function () SetNoteColourFore (ColourNameToRGB (CODECOLOUR))    end;
  ["<ul>"]    = function ()   end;
  ["<li/>"]   = function () Tell (" * ") end;
  ["<hr/>"]   = function () NoteHr () end;

  } -- end of tag_handlers

-- called by LPEG to output an opening, closing, or empty tag
local function tag_output (tag)

  -- restore colour on closing tag, save colour on nested tag
  if tag:sub (2, 2) == "/" then
    SetNoteColourFore (table.remove (styles))
  elseif tag:sub (-2, -2) ~= "/" then
    table.insert (styles, GetNoteColourFore () )
  end -- if

  local f = tag_handlers [tag:lower ()]
  if f then
    f ()
  end -- if handler found

end -- tag_output

-- called by LPEG to output straight text
local function text_output (text)
  -- fix up entities like &lt;
  Tell ((text:gsub ("&%a-;", entities)))
end -- text_output

-- LPEG grammar for (simplified) HTML
local P, R, S, C, Cc, Ct = lpeg.P, lpeg.R, lpeg.S, lpeg.C, lpeg.Cc, lpeg.Ct
local TAG     = (P"<" * P"/"^-1 * R("AZ", "az")^1 * P"/"^-1 * P">")/tag_output
local NOT_TAG = ((1 - lpeg.P"<")^0/text_output)
local HTML    = (NOT_TAG * TAG * NOT_TAG)^1 + NOT_TAG

-- show a help item with a coloured heading, and coloured text

local function show_item (hdg_colour, hdg, text_colour, text, tagged)

  if text and text ~= "" then

    if hdg and hdg ~= "" then
      ColourNote (hdg_colour, "", hdg)
      print ""
    end -- if any heading

    if tagged then
      local notecolour = GetNoteColourFore ()
      styles = {}  -- nested styles - start off with none
      SetNoteColourFore (ColourNameToRGB (text_colour))
      lpeg.match (HTML, text)
      SetNoteColourFore (notecolour)
      print ""  -- probably doesn't end with newline
    else
      ColourNote (text_colour, "", text)
    end -- if tagged or not

    print ""
  end -- if any text

end -- show_item

-- normal functions
local function show_functions_help (row)
  NoteHr ()
  show_item (HEADINGCOLOUR, "Function",      NAMECOLOUR,        row.name)
  show_item (HEADINGCOLOUR, "Summary",       SUMMARYCOLOUR,     row.summary)
  show_item (HEADINGCOLOUR, "Prototype",     PROTOTYPECOLOUR,   row.prototype)
  show_item (HEADINGCOLOUR, "Description",   DESCRIPTIONCOLOUR, row.description)
  show_item (HEADINGCOLOUR, "Return Value",  RETURNVALUE,       row.return_value)
  show_item (HEADINGCOLOUR, "Lua Example",   CODECOLOUR,        row.lua_example)
  show_item (HEADINGCOLOUR, "Lua Notes",     NOTESCOLOUR,       row.lua_notes)
end -- show_functions_help

-- general help (major topics)
local function show_general_help (row)
  NoteHr ()
  show_item (NOTESCOLOUR,   "",         NAMECOLOUR,     row.title)
  show_item (HEADINGCOLOUR, "Summary",  SUMMARYCOLOUR,  row.description,   true)  -- HTML codes
end -- show_general_help

-- lua functions
local function show_lua_functions_help (row)
  NoteHr ()
  show_item (HEADINGCOLOUR, "Lua",         NAMECOLOUR,        row.name)
  show_item (HEADINGCOLOUR, "Summary",     SUMMARYCOLOUR,     row.summary)
  show_item (HEADINGCOLOUR, "Prototype",   PROTOTYPECOLOUR,   row.prototype)
  show_item (HEADINGCOLOUR, "Description", DESCRIPTIONCOLOUR, row.description,  true) -- HTML codes
end -- show_lua_functions_help


local hyperlink_handlers = {

  functions     = { sql = "SELECT * FROM functions WHERE name = %s",
                    handler = show_functions_help };

  general       = { sql = "SELECT * FROM general_doc WHERE doc_name = %s",
                    handler = show_general_help };

  lua_functions = { sql = "SELECT * FROM lua_functions WHERE name = %s",
                    handler = show_lua_functions_help };

  } -- end of hyperlink_handlers

-- here when hyperlink clicked

function do_hyperlink (s)
  local what, name = string.match (s, "([%a_]+):([%a%d_%. :]+)")

  if not what then return end

  local help = hyperlink_handlers [what]

  if not help then
    ColourNote ("red", "", "Handler not found for " .. what)
    return
  end -- if not found

  -- should only find one match here ...
  for row in db:nrows(string.format (help.sql, fixsql (name))) do
     help.handler (row)
  end   -- finding function

end -- do_hyperlink

help_items = {}

function general_help (search_string, want_snippet)

  local searchfor = fixsql (search_string)

  AnsiNote (RESET)

  if not db then
     ColourNote ("red", "", "MUSHclient help database 'help.db' not found.")
     return
  end -- if

  local count = 0
  local functions = {}
  local general = {}
  local lua_functions = {}
  help_items = {}
  local item_count = 0

  function show_snippet (what, name, title, extra, snippet)
    item_count = item_count + 1
    help_items [item_count] = what .. ":" .. name
    Tell " "
    Hyperlink ("!!" .. GetPluginID () .. ":do_hyperlink(" .. what .. ":" .. name .. ")",
                item_count .. ". " .. title, "Click for help on " .. title, "cyan", "", false)
    if extra ~= "" then
        Tell (" - ", extra)
    end -- if
    print ""

    -- show snippet

    if want_snippet then
      -- get rid of newlines, and multiple spaces
      AnsiNote (RESET .. string.gsub (snippet, "%s+", " "))
      print ""
    end -- if snippet wanted

  end -- show_snippet

  NoteHr ()

  -- GENERAL documentation

  -- find matching items using FTS
  for row in db:nrows(string.format (
     [[
     SELECT name, summary, snippet(general_doc_lookup, '%s', '%s', ' ... ', -1, %i) AS snippet
        FROM general_doc_lookup
        WHERE general_doc_lookup MATCH %s]],
      BOLD, UNBOLD, SNIPPETSIZE,
     searchfor)) do
     general [row.name] = { snippet = row.snippet, summary = row.summary }
     count = count + 1
  end   -- finding item

  if next (general) then
    ColourNote (HEADINGCOLOUR, "", "Topics")
    for k, v in pairsByKeys (general) do
      show_snippet ("general", k, v.summary, "", v.snippet)
    end -- for
  end -- if

 -- FUNCTIONS documentation

  -- find matching items using FTS
  for row in db:nrows(string.format (
     [[
     SELECT name, summary, snippet(functions_lookup, '%s', '%s', ' ... ', -1, %i) AS snippet
        FROM functions_lookup
        WHERE functions_lookup MATCH %s]],
      BOLD, UNBOLD, SNIPPETSIZE,
      searchfor)) do
     functions [row.name] = { snippet = row.snippet, summary = row.summary }
     count = count + 1
  end   -- finding item

  if next (functions) then
    ColourNote (HEADINGCOLOUR, "", "Functions")
    for k, v in pairsByKeys (functions) do
      show_snippet ("functions", k, k, v.summary, v.snippet)
    end -- for
  end -- if

 -- LUA FUNCTIONS documentation

  -- find matching items using FTS
  for row in db:nrows(string.format (
     [[
     SELECT name, summary, snippet(lua_functions_lookup, '%s', '%s', ' ... ', -1, %i) AS snippet
        FROM lua_functions_lookup
        WHERE lua_functions_lookup MATCH %s]],
      BOLD, UNBOLD, SNIPPETSIZE,
      searchfor)) do
     lua_functions [row.name] = { snippet = row.snippet, summary = row.summary }
     count = count + 1
  end   -- finding item

  if next (lua_functions) then
    ColourNote (HEADINGCOLOUR, "", "Lua")
    for k, v in pairsByKeys (lua_functions) do
      show_snippet ("lua_functions", k, k, v.summary, v.snippet)
    end -- for
  end -- if

  if count == 0 then
    print ("Nothing found for:", search_string)
  elseif item_count == 1 then
    do_hyperlink (help_items [1])  -- automatically show only match
  end -- if

end -- general_help

-- here when they want to search the help

function help_find (name, line, wildcards)
  general_help (wildcards [1], false)
end -- help_find

function help_find_snippet (name, line, wildcards)
  general_help (wildcards [1], true)
end -- help_find_snippet

local function fix_description (s)
  if not s then
    return ""
  end -- if

  -- get rid of tags
  s = s:gsub ("</?%a+/?>", "")

  -- convert entities ...
  s = s:gsub ("&%a-;", entities)

  return s
end -- fix_description

function help_number (name, line, wildcards)
  local which = tonumber (wildcards [1])

  if #help_items == 0 then
    print ("No recent search done which gave any results")
    return
  end -- if

  if which < 1 or which > #help_items then
    print (string.format ("Requested help item %i not recently displayed - valid range is 1 to %i",
            which, #help_items))
    return
  end -- if

  do_hyperlink (help_items [which])

end -- help_number

function OnPluginInstall ()
  -- open database on disk

  if not db then
    db = assert (sqlite3.open(GetInfo (66) .. "help.db"))
  end -- if

  local commands = false

  -- see if commands table exists
  for row in db:nrows("SELECT * FROM sqlite_master WHERE type = 'table' AND name = 'commands'") do
    commands = true
  end

  if not commands then
    ColourNote ("red", "", "MUSHclient help database 'help.db' not found.")
    db:close ()
    db = nil
    return
  end -- if

  local fts4 = false

  -- see if fts4 tables exist
  for row in db:nrows("SELECT * FROM sqlite_master WHERE type = 'table' AND name = 'commands_lookup'") do
    fts4 = true
  end

    -- if no fts4 tables, make them
  if not fts4 then
    local start = utils.timer ()
    ColourNote ("cyan", "", "Creating help database full-text lookup tables ...")

    -- START
    assert (db:execute [[
      BEGIN TRANSACTION;
      DROP TABLE IF EXISTS commands_lookup;
      DROP TABLE IF EXISTS dialogs_lookup;
      DROP TABLE IF EXISTS functions_lookup;
      DROP TABLE IF EXISTS general_doc_lookup;
      DROP TABLE IF EXISTS errors_lookup;
      DROP TABLE IF EXISTS lua_functions_lookup;
    ]])

    -- COMMANDS
    assert (db:execute "CREATE VIRTUAL TABLE commands_lookup USING FTS4(name, summary, description)")

    -- fix up HTML stuff
    for row in db:nrows("SELECT command_name, short_description, description FROM commands") do
      assert (db:execute (string.format ([[
      INSERT INTO commands_lookup (name, summary, description)
             VALUES (%s, %s, %s)]],
             fixsql (row.command_name),
             fixsql (row.short_description),
             fixsql (fix_description (row.description)))))
    end -- for

    -- DIALOGS
    assert (db:execute "CREATE VIRTUAL TABLE dialogs_lookup USING FTS4(name, summary, description)")

    -- fix up HTML stuff
    for row in db:nrows("SELECT dialog_name, title, description FROM dialogs") do
      assert (db:execute (string.format ([[
      INSERT INTO dialogs_lookup (name, summary, description)
             VALUES (%s, %s, %s)]],
             fixsql (row.dialog_name),
             fixsql (row.title),
             fixsql (fix_description (row.description)))))
    end -- for

    -- WORLD FUNCTIONS
    assert (db:execute "CREATE VIRTUAL TABLE functions_lookup USING FTS4(name, summary, description, lua_example, lua_notes)")
    assert (db:execute [[INSERT INTO functions_lookup (name, summary, description, lua_example, lua_notes)
                         SELECT name, summary, description, lua_example, lua_notes FROM functions]])

    -- GENERAL TOPICS
    assert (db:execute "CREATE VIRTUAL TABLE general_doc_lookup USING FTS4(name, summary, description)")

    -- fix up HTML stuff
    for row in db:nrows("SELECT doc_name, title, description FROM general_doc") do
      assert (db:execute (string.format ([[
      INSERT INTO general_doc_lookup (name, summary, description)
             VALUES (%s, %s, %s)]],
             fixsql (row.doc_name),
             fixsql (row.title),
             fixsql (fix_description (row.description)))))
    end -- for

    -- ERRORS
    assert (db:execute "CREATE VIRTUAL TABLE errors_lookup USING FTS4(name, error_code, description)")
    assert (db:execute [[INSERT INTO errors_lookup (name, error_code, description)
                         SELECT error_name, error_code, meaning FROM errors ]])

    -- LUA FUNCTIONS
    assert (db:execute "CREATE VIRTUAL TABLE lua_functions_lookup USING FTS4(name, summary, description)")

    -- fix up HTML stuff
    for row in db:nrows("SELECT name, summary, description FROM lua_functions") do
      assert (db:execute (string.format ([[
      INSERT INTO lua_functions_lookup (name, summary, description)
             VALUES (%s, %s, %s)]],
             fixsql (row.name),
             fixsql (row.summary),
             fixsql (fix_description (row.description)))))
    end -- for

    -- DONE
    assert (db:execute "COMMIT;")
    ColourNote ("cyan", "", string.format ("Done. Took %0.3f seconds.", utils.timer () - start))

  end -- if

end -- OnPluginInstall

function OnHelp ()
  NoteHr ()
  ColourNote (HELPCOLOUR, "", world.GetPluginInfo (world.GetPluginID (), 3))
end

]]>
</script>

</muclient>
