Browse Source

initial commit

moononournation 7 years ago
commit
3ef802bb57

+ 340 - 0
LICENSE

@@ -0,0 +1,340 @@
+GNU GENERAL PUBLIC LICENSE
+                       Version 2, June 1991
+
+ Copyright (C) 1989, 1991 Free Software Foundation, Inc., <http://fsf.org/>
+ 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+                            Preamble
+
+  The licenses for most software are designed to take away your
+freedom to share and change it.  By contrast, the GNU General Public
+License is intended to guarantee your freedom to share and change free
+software--to make sure the software is free for all its users.  This
+General Public License applies to most of the Free Software
+Foundation's software and to any other program whose authors commit to
+using it.  (Some other Free Software Foundation software is covered by
+the GNU Lesser General Public License instead.)  You can apply it to
+your programs, too.
+
+  When we speak of free software, we are referring to freedom, not
+price.  Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+this service if you wish), that you receive source code or can get it
+if you want it, that you can change the software or use pieces of it
+in new free programs; and that you know you can do these things.
+
+  To protect your rights, we need to make restrictions that forbid
+anyone to deny you these rights or to ask you to surrender the rights.
+These restrictions translate to certain responsibilities for you if you
+distribute copies of the software, or if you modify it.
+
+  For example, if you distribute copies of such a program, whether
+gratis or for a fee, you must give the recipients all the rights that
+you have.  You must make sure that they, too, receive or can get the
+source code.  And you must show them these terms so they know their
+rights.
+
+  We protect your rights with two steps: (1) copyright the software, and
+(2) offer you this license which gives you legal permission to copy,
+distribute and/or modify the software.
+
+  Also, for each author's protection and ours, we want to make certain
+that everyone understands that there is no warranty for this free
+software.  If the software is modified by someone else and passed on, we
+want its recipients to know that what they have is not the original, so
+that any problems introduced by others will not reflect on the original
+authors' reputations.
+
+  Finally, any free program is threatened constantly by software
+patents.  We wish to avoid the danger that redistributors of a free
+program will individually obtain patent licenses, in effect making the
+program proprietary.  To prevent this, we have made it clear that any
+patent must be licensed for everyone's free use or not licensed at all.
+
+  The precise terms and conditions for copying, distribution and
+modification follow.
+
+                    GNU GENERAL PUBLIC LICENSE
+   TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
+
+  0. This License applies to any program or other work which contains
+a notice placed by the copyright holder saying it may be distributed
+under the terms of this General Public License.  The "Program", below,
+refers to any such program or work, and a "work based on the Program"
+means either the Program or any derivative work under copyright law:
+that is to say, a work containing the Program or a portion of it,
+either verbatim or with modifications and/or translated into another
+language.  (Hereinafter, translation is included without limitation in
+the term "modification".)  Each licensee is addressed as "you".
+
+Activities other than copying, distribution and modification are not
+covered by this License; they are outside its scope.  The act of
+running the Program is not restricted, and the output from the Program
+is covered only if its contents constitute a work based on the
+Program (independent of having been made by running the Program).
+Whether that is true depends on what the Program does.
+
+  1. You may copy and distribute verbatim copies of the Program's
+source code as you receive it, in any medium, provided that you
+conspicuously and appropriately publish on each copy an appropriate
+copyright notice and disclaimer of warranty; keep intact all the
+notices that refer to this License and to the absence of any warranty;
+and give any other recipients of the Program a copy of this License
+along with the Program.
+
+You may charge a fee for the physical act of transferring a copy, and
+you may at your option offer warranty protection in exchange for a fee.
+
+  2. You may modify your copy or copies of the Program or any portion
+of it, thus forming a work based on the Program, and copy and
+distribute such modifications or work under the terms of Section 1
+above, provided that you also meet all of these conditions:
+
+    a) You must cause the modified files to carry prominent notices
+    stating that you changed the files and the date of any change.
+
+    b) You must cause any work that you distribute or publish, that in
+    whole or in part contains or is derived from the Program or any
+    part thereof, to be licensed as a whole at no charge to all third
+    parties under the terms of this License.
+
+    c) If the modified program normally reads commands interactively
+    when run, you must cause it, when started running for such
+    interactive use in the most ordinary way, to print or display an
+    announcement including an appropriate copyright notice and a
+    notice that there is no warranty (or else, saying that you provide
+    a warranty) and that users may redistribute the program under
+    these conditions, and telling the user how to view a copy of this
+    License.  (Exception: if the Program itself is interactive but
+    does not normally print such an announcement, your work based on
+    the Program is not required to print an announcement.)
+
+These requirements apply to the modified work as a whole.  If
+identifiable sections of that work are not derived from the Program,
+and can be reasonably considered independent and separate works in
+themselves, then this License, and its terms, do not apply to those
+sections when you distribute them as separate works.  But when you
+distribute the same sections as part of a whole which is a work based
+on the Program, the distribution of the whole must be on the terms of
+this License, whose permissions for other licensees extend to the
+entire whole, and thus to each and every part regardless of who wrote it.
+
+Thus, it is not the intent of this section to claim rights or contest
+your rights to work written entirely by you; rather, the intent is to
+exercise the right to control the distribution of derivative or
+collective works based on the Program.
+
+In addition, mere aggregation of another work not based on the Program
+with the Program (or with a work based on the Program) on a volume of
+a storage or distribution medium does not bring the other work under
+the scope of this License.
+
+  3. You may copy and distribute the Program (or a work based on it,
+under Section 2) in object code or executable form under the terms of
+Sections 1 and 2 above provided that you also do one of the following:
+
+    a) Accompany it with the complete corresponding machine-readable
+    source code, which must be distributed under the terms of Sections
+    1 and 2 above on a medium customarily used for software interchange; or,
+
+    b) Accompany it with a written offer, valid for at least three
+    years, to give any third party, for a charge no more than your
+    cost of physically performing source distribution, a complete
+    machine-readable copy of the corresponding source code, to be
+    distributed under the terms of Sections 1 and 2 above on a medium
+    customarily used for software interchange; or,
+
+    c) Accompany it with the information you received as to the offer
+    to distribute corresponding source code.  (This alternative is
+    allowed only for noncommercial distribution and only if you
+    received the program in object code or executable form with such
+    an offer, in accord with Subsection b above.)
+
+The source code for a work means the preferred form of the work for
+making modifications to it.  For an executable work, complete source
+code means all the source code for all modules it contains, plus any
+associated interface definition files, plus the scripts used to
+control compilation and installation of the executable.  However, as a
+special exception, the source code distributed need not include
+anything that is normally distributed (in either source or binary
+form) with the major components (compiler, kernel, and so on) of the
+operating system on which the executable runs, unless that component
+itself accompanies the executable.
+
+If distribution of executable or object code is made by offering
+access to copy from a designated place, then offering equivalent
+access to copy the source code from the same place counts as
+distribution of the source code, even though third parties are not
+compelled to copy the source along with the object code.
+
+  4. You may not copy, modify, sublicense, or distribute the Program
+except as expressly provided under this License.  Any attempt
+otherwise to copy, modify, sublicense or distribute the Program is
+void, and will automatically terminate your rights under this License.
+However, parties who have received copies, or rights, from you under
+this License will not have their licenses terminated so long as such
+parties remain in full compliance.
+
+  5. You are not required to accept this License, since you have not
+signed it.  However, nothing else grants you permission to modify or
+distribute the Program or its derivative works.  These actions are
+prohibited by law if you do not accept this License.  Therefore, by
+modifying or distributing the Program (or any work based on the
+Program), you indicate your acceptance of this License to do so, and
+all its terms and conditions for copying, distributing or modifying
+the Program or works based on it.
+
+  6. Each time you redistribute the Program (or any work based on the
+Program), the recipient automatically receives a license from the
+original licensor to copy, distribute or modify the Program subject to
+these terms and conditions.  You may not impose any further
+restrictions on the recipients' exercise of the rights granted herein.
+You are not responsible for enforcing compliance by third parties to
+this License.
+
+  7. If, as a consequence of a court judgment or allegation of patent
+infringement or for any other reason (not limited to patent issues),
+conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License.  If you cannot
+distribute so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you
+may not distribute the Program at all.  For example, if a patent
+license would not permit royalty-free redistribution of the Program by
+all those who receive copies directly or indirectly through you, then
+the only way you could satisfy both it and this License would be to
+refrain entirely from distribution of the Program.
+
+If any portion of this section is held invalid or unenforceable under
+any particular circumstance, the balance of the section is intended to
+apply and the section as a whole is intended to apply in other
+circumstances.
+
+It is not the purpose of this section to induce you to infringe any
+patents or other property right claims or to contest validity of any
+such claims; this section has the sole purpose of protecting the
+integrity of the free software distribution system, which is
+implemented by public license practices.  Many people have made
+generous contributions to the wide range of software distributed
+through that system in reliance on consistent application of that
+system; it is up to the author/donor to decide if he or she is willing
+to distribute software through any other system and a licensee cannot
+impose that choice.
+
+This section is intended to make thoroughly clear what is believed to
+be a consequence of the rest of this License.
+
+  8. If the distribution and/or use of the Program is restricted in
+certain countries either by patents or by copyrighted interfaces, the
+original copyright holder who places the Program under this License
+may add an explicit geographical distribution limitation excluding
+those countries, so that distribution is permitted only in or among
+countries not thus excluded.  In such case, this License incorporates
+the limitation as if written in the body of this License.
+
+  9. The Free Software Foundation may publish revised and/or new versions
+of the General Public License from time to time.  Such new versions will
+be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+Each version is given a distinguishing version number.  If the Program
+specifies a version number of this License which applies to it and "any
+later version", you have the option of following the terms and conditions
+either of that version or of any later version published by the Free
+Software Foundation.  If the Program does not specify a version number of
+this License, you may choose any version ever published by the Free Software
+Foundation.
+
+  10. If you wish to incorporate parts of the Program into other free
+programs whose distribution conditions are different, write to the author
+to ask for permission.  For software which is copyrighted by the Free
+Software Foundation, write to the Free Software Foundation; we sometimes
+make exceptions for this.  Our decision will be guided by the two goals
+of preserving the free status of all derivatives of our free software and
+of promoting the sharing and reuse of software generally.
+
+                            NO WARRANTY
+
+  11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
+FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW.  EXCEPT WHEN
+OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
+PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
+OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE.  THE ENTIRE RISK AS
+TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU.  SHOULD THE
+PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
+REPAIR OR CORRECTION.
+
+  12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
+REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
+INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
+OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
+TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
+YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
+PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
+POSSIBILITY OF SUCH DAMAGES.
+
+                     END OF TERMS AND CONDITIONS
+
+            How to Apply These Terms to Your New Programs
+
+  If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+  To do so, attach the following notices to the program.  It is safest
+to attach them to the start of each source file to most effectively
+convey the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+    {description}
+    Copyright (C) {year}  {fullname}
+
+    This program is free software; you can redistribute it and/or modify
+    it under the terms of the GNU General Public License as published by
+    the Free Software Foundation; either version 2 of the License, or
+    (at your option) any later version.
+
+    This program is distributed in the hope that it will be useful,
+    but WITHOUT ANY WARRANTY; without even the implied warranty of
+    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+    GNU General Public License for more details.
+
+    You should have received a copy of the GNU General Public License along
+    with this program; if not, write to the Free Software Foundation, Inc.,
+    51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
+
+Also add information on how to contact you by electronic and paper mail.
+
+If the program is interactive, make it output a short notice like this
+when it starts in an interactive mode:
+
+    Gnomovision version 69, Copyright (C) year name of author
+    Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
+    This is free software, and you are welcome to redistribute it
+    under certain conditions; type `show c' for details.
+
+The hypothetical commands `show w' and `show c' should show the appropriate
+parts of the General Public License.  Of course, the commands you use may
+be called something other than `show w' and `show c'; they could even be
+mouse-clicks or menu items--whatever suits your program.
+
+You should also get your employer (if you work as a programmer) or your
+school, if any, to sign a "copyright disclaimer" for the program, if
+necessary.  Here is a sample; alter the names:
+
+  Yoyodyne, Inc., hereby disclaims all copyright interest in the program
+  `Gnomovision' (which makes passes at compilers) written by James Hacker.
+
+  {signature of Ty Coon}, 1 April 1989
+  Ty Coon, President of Vice
+
+This General Public License does not permit incorporating your program into
+proprietary programs.  If your program is a subroutine library, you may
+consider it more useful to permit linking proprietary applications with the
+library.  If this is what you want to do, use the GNU Lesser General
+Public License instead of this License.
+

+ 8 - 0
README.md

@@ -0,0 +1,8 @@
+nodemcu-webide
+
+Base on 2 core projects:
+# [nodemcu-httpserver](https://github.com/marcoskirsch/nodemcu-httpserver)
+A (very) simple web server written in Lua for the ESP8266 running the NodeMCU firmware.
+
+# [CodeMirror](https://codemirror.net)
+A versatile text editor implemented in JavaScript for the browser.

+ 3 - 0
TODO.md

@@ -0,0 +1,3 @@
+#TODO
+
+* list out TODO items

BIN
bin/codemirror-compressed.js.gz


+ 14 - 0
bin/config.lua

@@ -0,0 +1,14 @@
+-- Part of nodemcu-httpserver, contains static configuration for httpserver.
+-- Author: Sam Dieck
+
+local conf = {}
+
+-- Basic Authentication Conf
+local auth = {}
+auth.enabled = true
+auth.realm = "nodemcu-httpserver" -- displayed in the login dialog users get
+auth.user = "user"
+auth.password = "password" -- PLEASE change this
+conf.auth = auth
+
+return conf

BIN
bin/css.js.gz


BIN
bin/favicon.ico.gz


+ 57 - 0
bin/file-api.lua

@@ -0,0 +1,57 @@
+return function (connection, req, args)
+   dofile('httpserver-header.lc')(connection, 200, 'html')
+
+   if req.method == 'POST' then
+      --print('POST method')
+      local rd = req.getRequestData()
+      --print(node.heap())
+      collectgarbage()
+      --print(node.heap())
+      if rd['action'] == 'list' then
+         print('retrieve file list')
+         local l, filelist, n, s, ok, json
+         l = file.list()
+         filelist = {}
+         for n, s in pairs(l) do
+            if ((string.sub(n, -3) ~= '.gz') and (string.sub(n, -3) ~= '.lc')) then
+               filelist[n] = s
+            end
+         end
+         ok, json = pcall(cjson.encode, filelist)
+         if ok then
+            --print(json)
+            connection:send(json)
+         else
+            --print("failed to encode!")
+         end
+      elseif rd['action'] == 'load' then
+         print('load file: '..rd['filename'])
+         file.open(rd['filename'], 'r')
+         local buffer = file.read()
+         while buffer ~= nil do
+            connection:send(buffer)
+            buffer = file.read()
+         end
+         file.close()
+      elseif rd['action'] == 'save' then
+         print('save file: '..rd['filename'])
+         local data = rd['data']
+         file.open(rd['filename'], 'w+')
+         file.write(data)
+         file.close()
+         connection:send('initial write: ' .. string.len(data))
+      elseif rd['action'] == 'append' then
+         print('append file: '..rd['filename'])
+         local data = rd['data']
+         file.open(rd['filename'], 'a+')
+         file.seek('end')
+         file.write(data)
+         file.close()
+         connection:send('append: ' .. string.len(data))
+      elseif rd['action'] == 'compile' then
+         print('compile file: '..rd['filename'])
+         node.compile(rd['filename'])
+      end
+   end
+   collectgarbage()
+end

BIN
bin/htmlmixed.js.gz


+ 65 - 0
bin/httpserver-b64decode.lua

@@ -0,0 +1,65 @@
+#!/usr/local/bin/lua
+-- httpserver-b64decode.lua
+-- Part of nodemcu-httpserver, contains b64 decoding used for HTTP Basic Authentication.
+-- Based on http://lua-users.org/wiki/BaseSixtyFour by Alex Kloss
+-- compatible with lua 5.1
+-- http://www.it-rfc.de
+-- Author: Marcos Kirsch
+
+-- bitshift functions (<<, >> equivalent)
+-- shift left
+local function lsh(value,shift)
+   return (value*(2^shift)) % 256
+end
+
+-- shift right
+local function rsh(value,shift)
+   -- Lua builds with no floating point don't define math.
+   if math then return math.floor(value/2^shift) % 256 end
+   return (value/2^shift) % 256
+end
+
+-- return single bit (for OR)
+local function bit(x,b)
+   return (x % 2^b - x % 2^(b-1) > 0)
+end
+
+-- logic OR for number values
+local function lor(x,y)
+   result = 0
+   for p=1,8 do result = result + (((bit(x,p) or bit(y,p)) == true) and 2^(p-1) or 0) end
+   return result
+end
+
+-- Character decoding table
+local function toBase64Byte(char)
+   ascii = string.byte(char, 1)
+   if ascii >= string.byte('A', 1) and ascii <= string.byte('Z', 1) then return ascii - string.byte('A', 1)
+   elseif ascii >= string.byte('a', 1) and ascii <= string.byte('z', 1) then return ascii - string.byte('a', 1) + 26
+   elseif ascii >= string.byte('0', 1) and ascii <= string.byte('9', 1) then return ascii + 4
+   elseif ascii == string.byte('-', 1) then return 62
+   elseif ascii == string.byte('_', 1) then return 63
+   elseif ascii == string.byte('=', 1) then return nil
+   else return nil, "ERROR! Char is invalid for Base64 encoding: "..char end
+end
+
+
+-- decode base64 input to string
+return function(data)
+   local chars = {}
+   local result=""
+   for dpos=0,string.len(data)-1,4 do
+      for char=1,4 do chars[char] = toBase64Byte((string.sub(data,(dpos+char),(dpos+char)) or "=")) end
+         result = string.format(
+         '%s%s%s%s',
+         result,
+         string.char(lor(lsh(chars[1],2), rsh(chars[2],4))),
+         (chars[3] ~= nil) and string.char(lor(lsh(chars[2],4),
+         rsh(chars[3],2))) or "",
+         (chars[4] ~= nil) and string.char(lor(lsh(chars[3],6) % 192,
+         (chars[4]))) or ""
+      )
+   end
+   return result
+end
+

+ 31 - 0
bin/httpserver-basicauth.lua

@@ -0,0 +1,31 @@
+-- httpserver-basicauth.lua
+-- Part of nodemcu-httpserver, authenticates a user using http basic auth.
+-- Author: Sam Dieck
+
+basicAuth = {}
+
+-- Parse basic auth http header.
+-- Returns the username if header contains valid credentials,
+-- nil otherwise.
+function basicAuth.authenticate(header)
+   local conf = dofile("config.lua")
+   local credentials_enc = header:match("Authorization: Basic ([A-Za-z0-9+/=]+)")
+   if not credentials_enc then
+      return nil
+   end
+   local credentials = dofile("httpserver-b64decode.lc")(credentials_enc)
+   local user, pwd = credentials:match("^(.*):(.*)$")
+   if user ~= conf.auth.user or pwd ~= conf.auth.password then
+      print("httpserver-basicauth: User \"" .. user .. "\": Access denied.")
+      return nil
+   end
+   print("httpserver-basicauth: User \"" .. user .. "\": Authenticated.")
+   return user
+end
+
+function basicAuth.authErrorHeader()
+   local conf = dofile("config.lua")
+   return "WWW-Authenticate: Basic realm=\"" .. conf.auth.realm .. "\""
+end
+
+return basicAuth

+ 67 - 0
bin/httpserver-connection.lua

@@ -0,0 +1,67 @@
+-- httpserver-connection
+-- Part of nodemcu-httpserver, provides a buffered connection object that can handle multiple
+-- consecutive send() calls, and buffers small payloads to send once they get big.
+-- For this to work, it must be used from a coroutine and owner is responsible for the final
+-- flush() and for closing the connection.
+-- Author: Philip Gladstone, Marcos Kirsch
+
+BufferedConnection = {}
+
+-- parameter is the nodemcu-firmware connection
+function BufferedConnection:new(connection)
+   local newInstance = {}
+   newInstance.connection = connection
+   newInstance.size = 0
+   newInstance.data = {}
+
+   function newInstance:flush()
+      if self.size > 0 then
+         self.connection:send(table.concat(self.data, ""))
+         self.data = {}
+         self.size = 0
+         return true
+      end
+      return false
+   end
+
+   function newInstance:send(payload)
+      local flushthreshold = 1400
+
+      local newsize = self.size + payload:len()
+      while newsize > flushthreshold do
+          --STEP1: cut out piece from payload to complete threshold bytes in table
+          local piecesize = flushthreshold - self.size
+          local piece = payload:sub(1, piecesize)
+          payload = payload:sub(piecesize + 1, -1)
+          --STEP2: insert piece into table
+          table.insert(self.data, piece)
+          self.size = self.size + piecesize --size should be same as flushthreshold
+          --STEP3: flush entire table
+          if self:flush() then
+              coroutine.yield()
+          end
+          --at this point, size should be 0, because the table was just flushed
+          newsize = self.size + payload:len()
+      end
+            
+      --at this point, whatever is left in payload should be <= flushthreshold
+      local plen = payload:len()
+      if plen == flushthreshold then
+          --case 1: what is left in payload is exactly flushthreshold bytes (boundary case), so flush it
+          table.insert(self.data, payload)
+          self.size = self.size + plen
+          if self:flush() then
+              coroutine.yield()
+          end
+      elseif payload:len() then
+          --case 2: what is left in payload is less than flushthreshold, so just leave it in the table
+          table.insert(self.data, payload)
+          self.size = self.size + plen
+      --else, case 3: nothing left in payload, so do nothing
+      end
+   end
+
+   return newInstance
+end
+
+return BufferedConnection

+ 22 - 0
bin/httpserver-error.lua

@@ -0,0 +1,22 @@
+-- httpserver-error.lua
+-- Part of nodemcu-httpserver, handles sending error pages to client.
+-- Author: Marcos Kirsch
+
+return function (connection, req, args)
+
+   -- @TODO: would be nice to use httpserver-header.lua
+   local function getHeader(connection, code, errorString, extraHeaders, mimeType)
+      local header = "HTTP/1.0 " .. code .. " " .. errorString .. "\r\nServer: nodemcu-httpserver\r\nContent-Type: " .. mimeType .. "\r\n"
+      for i, extraHeader in ipairs(extraHeaders) do
+         header = header .. extraHeader .. "\r\n"
+      end
+      header = header .. "connection: close\r\n\r\n"
+      return header
+   end
+
+   print("Error " .. args.code .. ": " .. args.errorString)
+   args.headers = args.headers or {}
+   connection:send(getHeader(connection, args.code, args.errorString, args.headers, "text/html"))
+   connection:send("<html><head><title>" .. args.code .. " - " .. args.errorString .. "</title></head><body><h1>" .. args.code .. " - " .. args.errorString .. "</h1></body></html>\r\n")
+
+end

+ 30 - 0
bin/httpserver-header.lua

@@ -0,0 +1,30 @@
+-- httpserver-header.lua
+-- Part of nodemcu-httpserver, knows how to send an HTTP header.
+-- Author: Marcos Kirsch
+
+return function (connection, code, extension, isGzipped)
+
+   local function getHTTPStatusString(code)
+      local codez = {[200]="OK", [304]="Not Modified", [400]="Bad Request", [404]="Not Found",}
+      local myResult = codez[code]
+      -- enforce returning valid http codes all the way throughout?
+      if myResult then return myResult else return "Not Implemented" end
+   end
+
+   local function getMimeType(ext)
+      -- A few MIME types. Keep list short. If you need something that is missing, let's add it.
+      local mt = {css = "text/css", gif = "image/gif", html = "text/html", ico = "image/x-icon", jpeg = "image/jpeg", jpg = "image/jpeg", js = "application/javascript", json = "application/json", png = "image/png", xml = "text/xml", svg = "image/svg+xml"}
+      if mt[ext] then return mt[ext] else return "text/plain" end
+   end
+
+   local mimeType = getMimeType(extension)
+
+   connection:send("HTTP/1.0 " .. code .. " " .. getHTTPStatusString(code) .. "\r\nServer: nodemcu-httpserver\r\nContent-Type: " .. mimeType .. "\r\nnCache-Control: private, no-store\r\n")
+   if isGzipped then
+      connection:send("Cache-Control: max-age=2592000\r\nContent-Encoding: gzip\r\n")
+   end
+   connection:send("Last-Modified: Fri, 01 Jan 2016 00:00:00 GMT\r\n")
+   connection:send("Connection: close\r\n\r\n")
+
+end
+

+ 127 - 0
bin/httpserver-request.lua

@@ -0,0 +1,127 @@
+-- httpserver-request
+-- Part of nodemcu-httpserver, parses incoming client requests.
+-- Author: Marcos Kirsch
+
+local function validateMethod(method)
+   local httpMethods = {GET=true, HEAD=true, POST=true, PUT=true, DELETE=true, TRACE=true, OPTIONS=true, CONNECT=true, PATCH=true}
+   -- default for non-existent attributes returns nil, which evaluates to false
+   return httpMethods[method]
+end
+
+local function uriToFilename(uri)
+   return string.sub(uri, 2, -1)
+end
+
+local function hex_to_char(x)
+  return string.char(tonumber(x, 16))
+end
+
+local function uri_decode(input)
+  return input:gsub("%+", " "):gsub("%%(%x%x)", hex_to_char)
+end
+
+local function parseArgs(args)
+   local r = {}; i=1
+   if args == nil or args == "" then return r end
+   for arg in string.gmatch(args, "([^&]+)") do
+      local name, value = string.match(arg, "(.*)=(.*)")
+      if name ~= nil then r[name] = uri_decode(value) end
+      i = i + 1
+   end
+   return r
+end
+
+local function parseFormData(body)
+   local data = {}
+   --print("Parsing Form Data")
+   for kv in body.gmatch(body, "%s*&?([^=]+=[^&]+)") do
+      local key, value = string.match(kv, "(.*)=(.*)")
+      --print("Parsed: " .. key .. " => " .. value)
+      data[key] = uri_decode(value)
+   end
+   return data
+end
+
+local function isCheckModifiedRequest(payload)
+   return function ()
+      local checkResult = (string.find(payload, 'If%-Modified%-Since: ') ~= nil)
+      return checkResult
+   end
+end
+
+local function getRequestData(payload)
+   local requestData
+   return function ()
+      --print("Getting Request Data")
+      if requestData then
+         return requestData
+      else
+         --print("payload = [" .. payload .. "]")
+         local mimeType = string.match(payload, "Content%-Type: ([%w/-]+)")
+         local bodyStart = payload:find("\r\n\r\n", 1, true)
+         local body = payload:sub(bodyStart, #payload)
+         payload = nil
+         collectgarbage()
+         --print("mimeType = [" .. mimeType .. "]")
+         --print("bodyStart = [" .. bodyStart .. "]")
+         --print("body = [" .. body .. "]")
+         if mimeType == "application/json" then
+            --print("JSON: " .. body)
+            requestData = cjson.decode(body)
+         elseif mimeType == "application/x-www-form-urlencoded" then
+            requestData = parseFormData(body)
+         else
+            requestData = {}
+         end
+         return requestData
+      end
+   end
+end
+
+local function parseUri(uri)
+   local r = {}
+   local filename
+   local ext
+   local fullExt = {}
+
+   if uri == nil then return r end
+   if uri == "/" then uri = "/index.html" end
+   questionMarkPos, b, c, d, e, f = uri:find("?")
+   if questionMarkPos == nil then
+      r.file = uri:sub(1, questionMarkPos)
+      r.args = {}
+   else
+      r.file = uri:sub(1, questionMarkPos - 1)
+      r.args = parseArgs(uri:sub(questionMarkPos+1, #uri))
+   end
+   filename = r.file
+   while filename:match("%.") do
+      filename,ext = filename:match("(.+)%.(.+)")
+      table.insert(fullExt,1,ext)
+   end
+   if #fullExt > 1 and fullExt[#fullExt] == 'gz' then
+      r.ext = fullExt[#fullExt-1]
+      r.isGzipped = true
+   elseif #fullExt >= 1 then
+      r.ext = fullExt[#fullExt]
+   end
+   r.isScript = r.ext == "lua" or r.ext == "lc"
+   r.file = uriToFilename(r.file)
+   return r
+end
+
+-- Parses the client's request. Returns a dictionary containing pretty much everything
+-- the server needs to know about the uri.
+return function (request)
+   --print("Request: \n", request)
+   local e = request:find("\r\n", 1, true)
+   if not e then return nil end
+   local line = request:sub(1, e - 1)
+   local r = {}
+   _, i, r.method, r.request = line:find("^([A-Z]+) (.-) HTTP/[1-9]+.[0-9]+$")
+   r.methodIsValid = validateMethod(r.method)
+   r.uri = parseUri(r.request)
+   r.isCheckModifiedRequest = isCheckModifiedRequest(request)
+   r.getRequestData = getRequestData(request)
+   return r
+end

+ 38 - 0
bin/httpserver-static.lua

@@ -0,0 +1,38 @@
+-- httpserver-static.lua
+-- Part of nodemcu-httpserver, handles sending static files to client.
+-- Author: Marcos Kirsch
+
+return function (connection, req, args)
+   --print("Begin sending:", args.file)
+   --print("node.heap(): ", node.heap())
+   if args.isGzipped and req.isCheckModifiedRequest() then
+      dofile("httpserver-header.lc")(connection, 304, args.ext, args.isGzipped)
+   else
+      dofile("httpserver-header.lc")(connection, 200, args.ext, args.isGzipped)
+      -- Send file in little chunks
+      local continue = true
+      local size = file.list()[args.file]
+      local bytesSent = 0
+      -- Chunks larger than 1024 don't work.
+      -- https://github.com/nodemcu/nodemcu-firmware/issues/1075
+      local chunkSize = 1024
+      while continue do
+         collectgarbage()
+
+         -- NodeMCU file API lets you open 1 file at a time.
+         -- So we need to open, seek, close each time in order
+         -- to support multiple simultaneous clients.
+         file.open(args.file)
+         file.seek("set", bytesSent)
+         local chunk = file.read(chunkSize)
+         file.close()
+
+         connection:send(chunk)
+         bytesSent = bytesSent + #chunk
+         chunk = nil
+         --print("Sent: " .. bytesSent .. " of " .. size)
+         if bytesSent == size then continue = false end
+      end
+      --print("Finished sending: ", args.file)
+   end
+end

+ 166 - 0
bin/httpserver.lua

@@ -0,0 +1,166 @@
+-- httpserver
+-- Author: Marcos Kirsch
+
+-- Starts web server in the specified port.
+return function (port)
+
+   local s = net.createServer(net.TCP, 10) -- 10 seconds client timeout
+   s:listen(
+      port,
+      function (connection)
+
+         -- This variable holds the thread (actually a Lua coroutine) used for sending data back to the user.
+         -- We do it in a separate thread because we need to send in little chunks and wait for the onSent event
+         -- before we can send more, or we risk overflowing the mcu's buffer.
+         local connectionThread
+
+         local allowStatic = {GET=true, HEAD=true, POST=false, PUT=false, DELETE=false, TRACE=false, OPTIONS=false, CONNECT=false, PATCH=false}
+
+         local function startServing(fileServeFunction, connection, req, args)
+            connectionThread = coroutine.create(function(fileServeFunction, bufferedConnection, req, args)
+               fileServeFunction(bufferedConnection, req, args)
+               -- The bufferedConnection may still hold some data that hasn't been sent. Flush it before closing.
+               if not bufferedConnection:flush() then
+                  connection:close()
+                  connectionThread = nil
+               end
+            end)
+
+            local BufferedConnectionClass = dofile("httpserver-connection.lc")
+            local bufferedConnection = BufferedConnectionClass:new(connection)
+            local status, err = coroutine.resume(connectionThread, fileServeFunction, bufferedConnection, req, args)
+            if not status then
+               print("Error: ", err)
+            end
+         end
+
+         local function handleRequest(connection, req)
+            collectgarbage()
+            local method = req.method
+            local uri = req.uri
+            local fileServeFunction = nil
+
+            if #(uri.file) > 32 then
+               -- nodemcu-firmware cannot handle long filenames.
+               uri.args = {code = 400, errorString = "Bad Request"}
+               fileServeFunction = dofile("httpserver-error.lc")
+            else
+               local fileExists = file.open(uri.file, "r")
+               file.close()
+
+               if not fileExists then
+                  -- gzip check
+                  fileExists = file.open(uri.file .. ".gz", "r")
+                  file.close()
+
+                  if fileExists then
+                     --print("gzip variant exists, serving that one")
+                     uri.file = uri.file .. ".gz"
+                     uri.isGzipped = true
+                  end
+               end
+
+               if not fileExists then
+                  uri.args = {code = 404, errorString = "Not Found"}
+                  fileServeFunction = dofile("httpserver-error.lc")
+               elseif uri.isScript then
+                  fileServeFunction = dofile(uri.file)
+               else
+                  if allowStatic[method] then
+                     uri.args = {file = uri.file, ext = uri.ext, isGzipped = uri.isGzipped}
+                     fileServeFunction = dofile("httpserver-static.lc")
+                  else
+                     uri.args = {code = 405, errorString = "Method not supported"}
+                     fileServeFunction = dofile("httpserver-error.lc")
+                  end
+               end
+            end
+            startServing(fileServeFunction, connection, req, uri.args)
+         end
+
+         local function onReceive(connection, payload)
+            collectgarbage()
+            local conf = dofile("config.lua")
+            local auth
+            local user = "Anonymous"
+
+            -- as suggest by anyn99 (https://github.com/marcoskirsch/nodemcu-httpserver/issues/36#issuecomment-167442461)
+            -- Some browsers send the POST data in multiple chunks.
+            -- Collect data packets until the size of HTTP body meets the Content-Length stated in header
+            if payload:find("Content%-Length:") or bBodyMissing then
+               if fullPayload then fullPayload = fullPayload .. payload else fullPayload = payload end
+               if (tonumber(string.match(fullPayload, "%d+", fullPayload:find("Content%-Length:")+16)) > #fullPayload:sub(fullPayload:find("\r\n\r\n", 1, true)+4, #fullPayload)) then
+                  bBodyMissing = true
+                  return
+               else
+                  --print("HTTP packet assembled! size: "..#fullPayload)
+                  payload = fullPayload
+                  fullPayload, bBodyMissing = nil
+               end
+            end
+            collectgarbage()
+
+            -- parse payload and decide what to serve.
+            local req = dofile("httpserver-request.lc")(payload)
+            --print(req.method .. ": " .. req.request)
+            if conf.auth.enabled then
+               auth = dofile("httpserver-basicauth.lc")
+               user = auth.authenticate(payload) -- authenticate returns nil on failed auth
+               req.user = user -- store authenticated user to req table
+            end
+
+            if user and req.methodIsValid and (req.method == "GET" or req.method == "POST" or req.method == "PUT") then
+               handleRequest(connection, req)
+            else
+               local args = {}
+               local fileServeFunction = dofile("httpserver-error.lc")
+               if not user then
+                  args = {code = 401, errorString = "Not Authorized", headers = {auth.authErrorHeader()}}
+               elseif req.methodIsValid then
+                  args = {code = 501, errorString = "Not Implemented"}
+               else
+                  args = {code = 400, errorString = "Bad Request"}
+               end
+               startServing(fileServeFunction, connection, req, args)
+            end
+         end
+
+         local function onSent(connection, payload)
+            collectgarbage()
+            if connectionThread then
+               local connectionThreadStatus = coroutine.status(connectionThread)
+               if connectionThreadStatus == "suspended" then
+                  -- Not finished sending file, resume.
+                  local status, err = coroutine.resume(connectionThread)
+                  if not status then
+                     print(err)
+                  end
+               elseif connectionThreadStatus == "dead" then
+                  -- We're done sending file.
+                  connection:close()
+                  connectionThread = nil
+               end
+            end
+         end
+
+         local function onDisconnect(connection, payload)
+            if connectionThread then
+               connectionThread = nil
+               collectgarbage()
+            end
+         end
+
+         connection:on("receive", onReceive)
+         connection:on("sent", onSent)
+         connection:on("disconnection", onDisconnect)
+
+      end
+   )
+   -- false and nil evaluate as false
+   local ip = wifi.sta.getip()
+   if not ip then ip = wifi.ap.getip() end
+   if not ip then ip = "unknown IP" end
+   print("nodemcu-httpserver running at http://" .. ip .. ":" ..  port)
+   return s
+
+end

BIN
bin/index.css.gz


BIN
bin/index.html.gz


BIN
bin/index.js.gz


+ 107 - 0
bin/init.lua

@@ -0,0 +1,107 @@
+ws2812.init()
+ws2812.write(string.char(0):rep(3096))
+
+-- Begin WiFi configuration
+
+local wifiConfig = {}
+
+-- wifi.STATION         -- station: join a WiFi network
+-- wifi.SOFTAP          -- access point: create a WiFi network
+-- wifi.STATIONAP       -- both station and access point
+wifiConfig.mode = wifi.STATIONAP  -- both station and access point
+
+wifiConfig.accessPointConfig = {}
+wifiConfig.accessPointConfig.ssid = "ESP-"..node.chipid()   -- Name of the SSID you want to create
+wifiConfig.accessPointConfig.pwd = "ESP-"..node.chipid()    -- WiFi password - at least 8 characters
+
+wifiConfig.accessPointIpConfig = {}
+wifiConfig.accessPointIpConfig.ip = "192.168.111.1"
+wifiConfig.accessPointIpConfig.netmask = "255.255.255.0"
+wifiConfig.accessPointIpConfig.gateway = "192.168.111.1" -- avoid mobile cannot access internet issue
+
+wifiConfig.stationPointConfig = {}
+wifiConfig.stationPointConfig.ssid = "YourSSID" -- Name of the WiFi network you want to join
+wifiConfig.stationPointConfig.pwd = "PleaseInputYourPasswordHere" -- Password for the WiFi network
+
+-- Tell the chip to connect to the access point
+
+wifi.setmode(wifiConfig.mode)
+print('set (mode='..wifi.getmode()..')')
+
+if (wifiConfig.mode == wifi.SOFTAP) or (wifiConfig.mode == wifi.STATIONAP) then
+    print('AP MAC: ',wifi.ap.getmac())
+    wifi.ap.config(wifiConfig.accessPointConfig)
+    wifi.ap.setip(wifiConfig.accessPointIpConfig)
+end
+if (wifiConfig.mode == wifi.STATION) or (wifiConfig.mode == wifi.STATIONAP) then
+    print('Client MAC: ',wifi.sta.getmac())
+    wifi.sta.config(wifiConfig.stationPointConfig.ssid, wifiConfig.stationPointConfig.pwd, 1)
+end
+
+print('chip: ',node.chipid())
+print('heap: ',node.heap())
+
+wifiConfig = nil
+collectgarbage()
+
+-- End WiFi configuration
+
+-- Compile server code and remove original .lua files.
+-- This only happens the first time afer the .lua files are uploaded.
+
+local compileAndRemoveIfNeeded = function(f)
+   if file.open(f) then
+      file.close()
+      print('Compiling:', f)
+      node.compile(f)
+      file.remove(f)
+      collectgarbage()
+   end
+end
+
+local serverFiles = {
+   'httpserver.lua',
+   'httpserver-b64decode.lua',
+   'httpserver-basicauth.lua',
+   'httpserver-connection.lua',
+   'httpserver-error.lua',
+   'httpserver-header.lua',
+   'httpserver-request.lua',
+   'httpserver-static.lua',
+   'file-api.lua'
+}
+for i, f in ipairs(serverFiles) do compileAndRemoveIfNeeded(f) end
+
+compileAndRemoveIfNeeded = nil
+serverFiles = nil
+collectgarbage()
+
+-- Connect to the WiFi access point.
+-- Once the device is connected, you may start the HTTP server.
+
+if (wifi.getmode() == wifi.STATION) or (wifi.getmode() == wifi.STATIONAP) then
+    local joinCounter = 0
+    local joinMaxAttempts = 5
+    tmr.alarm(0, 3000, 1, function()
+       local ip = wifi.sta.getip()
+       if ip == nil and joinCounter < joinMaxAttempts then
+          print('Connecting to WiFi Access Point ...')
+          joinCounter = joinCounter +1
+       else
+          if joinCounter == joinMaxAttempts then
+             print('Failed to connect to WiFi Access Point.')
+          else
+             print('IP: ',ip)
+          end
+          tmr.stop(0)
+          joinCounter = nil
+          joinMaxAttempts = nil
+          collectgarbage()
+       end
+    end)
+end
+
+-- Uncomment to automatically start the server in port 80
+if (not not wifi.sta.getip()) or (not not wifi.ap.getip()) then
+    dofile("httpserver.lc")(80)
+end

BIN
bin/javascript.js.gz


+ 149 - 0
bin/led-matrix.html

@@ -0,0 +1,149 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+<meta charset="utf-8">
+<title>LED Matrix</title>
+<style>
+#colorPicker {
+   padding: 4px;
+   background: #000;
+}
+#ledMatrix {
+   width: 320px;
+   height: 192px;
+   padding: 16px;
+   -webkit-border-radius:8px;
+   -moz-border-radius: 8px;
+   border-radius: 8px;
+   background: #C0C0C0;
+}
+.Table {
+   display: table;
+   margin: 2px;
+}
+.Row {
+   display: table-row;
+}
+.Cell {
+   display: table-cell;
+}
+.Color {
+   width: 20px;
+   height: 20px;
+   margin: 2px;
+}
+.Circle {
+   width: 16px;
+   height: 16px;
+   margin: 8px;
+   -webkit-border-radius: 8px;
+   -moz-border-radius: 8px;
+   border-radius: 8px;
+   background: #333;
+}
+</style>
+</head>
+<body>
+<h1>LED Matrix</h1>
+<div class="Table">
+<div class="Row">
+<div class="Cell">
+Color: <input type="text" id="colorText" size="10" value="#FFF" disabled>
+<div id="colorPicker" class="Table"></div>
+</div>
+<div class="Cell">
+<button id="btnSend">Send dots color to ESP-MINTIA</button>
+<div id="ledMatrix" class="Table"></div>
+</div>
+</div>
+</div>
+<div id="dataOutput"></div>
+<div id="txtResponse"></div>
+<script>
+var w = 10;
+var h = 6;
+var colorMin = 3;
+var colorMax = 15;
+var xhr;
+
+function getRGB(e) {
+   return window.getComputedStyle(e).backgroundColor.match(/\d+/g).map(function(a){ return Math.round(parseInt(a, 10) / 16.5); });
+}
+
+function getColor(e) {
+   var ct = document.getElementById("colorText");
+   var rgb = getRGB(e);
+   ct.value = "#" + rgb[0].toString(16) + rgb[1].toString(16) + rgb[2].toString(16);
+   ct.style.backgroundColor = ct.value;
+}
+
+function setColor(e) {
+   var ct = document.getElementById("colorText");
+   e.style.backgroundColor = ct.value;
+   sendData(e);
+}
+
+function sendData() {
+   var o = document.getElementById("dataOutput");
+   var text = "";
+   for (var i = 0; i < (h * w); i++) {
+      var d = document.getElementById("dot_" + i);
+      var rgb = getRGB(d);
+      text += String.fromCharCode(rgb[1] - colorMin);
+      text += String.fromCharCode(rgb[0] - colorMin);
+      text += String.fromCharCode(rgb[2] - colorMin);
+   }
+   text = "data=" + escape(text)
+   o.innerHTML = text;
+   if (!xhr) xhr = new XMLHttpRequest();
+   xhr.open("POST", "led-matrix.lc", true);
+   xhr.onreadystatechange=function() {
+      if (xhr.readyState === 4) {
+         var r = document.getElementById("txtResponse");
+         r.innerHTML = xhr.responseText;
+      }
+   }
+   xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+   xhr.send(text);
+}
+
+var cp = document.getElementById("colorPicker");
+var colorHtml = "";
+var i = 0;
+for (var r = colorMin; r <= colorMax; r+=6) {
+   for (var g = colorMin; g <= colorMax; g+=6) {
+      if ((g % 6) == 3) colorHtml += '<div calss="Row">';
+      for (var b = colorMin; b <= colorMax; b+=2) {
+         i++;
+         colorHtml += '<div class="Cell"><div class="Color" id="color' + i + '" style="background:#' + r.toString(16) + g.toString(16) + b.toString(16) + ';"></div></div>';
+      }
+      if ((g % 6) == 3) colorHtml += '</div>';
+   }
+}
+cp.innerHTML = colorHtml;
+for (var j = 1; j <= i; j++) {
+   var c = document.getElementById("color" + j);
+   c.addEventListener("click", function(e) {
+      getColor(e.srcElement);
+   });
+}
+
+var lm = document.getElementById("ledMatrix");
+var dotHtml = "";
+for (var i = 0; i < (h * w); i++) {
+   if ((i % w) == 0) dotHtml += '<div calss="Row">';
+   dotHtml += '<div class="Cell"><div class="Circle" id="dot_' + i + '"></div></div>';
+   if ((i % w) == (w - 1)) dotHtml += '</div>';
+}
+lm.innerHTML = dotHtml;
+for (var i = 0; i < (h * w); i++) {
+   var d = document.getElementById("dot_" + i);
+   d.addEventListener("click", function(e) {
+      setColor(e.srcElement);
+   });
+}
+
+document.getElementById("btnSend").addEventListener("click", sendData);
+</script>
+</body>
+</html>

+ 11 - 0
bin/led-matrix.lua

@@ -0,0 +1,11 @@
+return function (connection, req, args)
+   dofile('httpserver-header.lc')(connection, 200, 'html')
+
+   if req.method == 'POST' then
+      local rd = req.getRequestData()
+      if (rd['data'] ~= nil) then
+         ws2812.write(rd['data'])
+      end
+   end
+   collectgarbage()
+end

BIN
bin/lua.js.gz


BIN
bin/xml.js.gz


File diff suppressed because it is too large
+ 108 - 0
index.css


+ 25 - 0
index.html

@@ -0,0 +1,25 @@
+<!doctype html>
+<html>
+
+<head>
+  <meta charset="utf-8">
+  <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0">
+  <title>NodeMCU WebIDE</title>
+  <link rel="stylesheet" href="index.css" />
+</head>
+
+<body ng-app="webIDEApp">
+  <div id="toolbar">
+    <div class="header">NodeMCU WebIDE</div>
+    <button id="undo"><span class="icon icon-undo"></span></button>
+    <button id="save"><span class="icon icon-save"></span></button>
+    <button id="preview"><span class="icon icon-preview"></span></button>
+    <div id="localStatus"></div>
+    <div id="remoteStatus"></div>
+  </div>
+  <div id="filelist"></div>
+  <div id="editor"></div>
+  <script type="text/javascript" src="index.js"></script>
+</body>
+
+</html>

+ 225 - 0
index.js

@@ -0,0 +1,225 @@
+var blockSize = 1024;
+var normalColor = "#0067B0";
+var selectedColor = "#CCCCCC";
+var showInvisibles = false;
+var editor;
+var curFileItem;
+var savingText;
+var savingFilename;
+var savingFileOffset;
+var xhr; // reuse
+
+function setLocalStatus(msg) {
+  document.getElementById("localStatus").innerHTML = msg;
+}
+
+function setRemoteStatus(msg) {
+  document.getElementById("remoteStatus").innerHTML = msg;
+}
+
+function loadScript(url, callback) {
+  setLocalStatus("<span class=\"icon icon-loading\"></span> Loading " + url);
+
+  var script = document.createElement("script");
+  script.type = "text/javascript";
+
+  if (script.readyState) {  //IE
+    script.onreadystatechange = function () {
+      if (script.readyState == "loaded" || script.readyState == "complete") {
+        script.onreadystatechange = null;
+        callback();
+      }
+    };
+  } else { //Others
+    script.onload = function () {
+      setLocalStatus("");
+
+      callback();
+    };
+  }
+
+  script.src = url;
+  document.getElementsByTagName("head")[0].appendChild(script);
+}
+
+function isXhrSuccess(xhr) {
+  return ((xhr.readyState === 4) && (xhr.status == 200));
+}
+
+function handleFileClick(item) {
+  if (curFileItem) {
+    curFileItem.classList.remove("selected");
+  }
+  item.classList.add("selected");
+  curFileItem = item;
+
+  loadFile();
+}
+
+function loadFilelist() {
+  setLocalStatus("<span class=\"icon icon-loading\"></span> Loading file list");
+
+  if (!xhr) xhr = new XMLHttpRequest();
+  xhr.open("POST", "file-api.lc", true);
+  xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+  xhr.onreadystatechange = function () {
+    if (isXhrSuccess(xhr)) {
+      var filelistHtml = "";
+      var json = JSON.parse(xhr.responseText);
+      var files = new Array(json.length);
+      var i = 0;
+      for (var filename in json) {
+        files[i++] = filename;
+      }
+      files.sort();
+      for (i = 0; i < files.length; i++) {
+        filelistHtml += '<div class="fileItem" id="' + files[i] + '"><span class="icon icon-file"></span> ' + files[i] + '</div>'
+      }
+      document.getElementById("filelist").innerHTML = filelistHtml;
+
+      fileItemList = document.getElementsByClassName("fileItem");
+      for (i = 0; i < fileItemList.length; i++) {
+        fileItemList[i].addEventListener("click", function (e) {
+          handleFileClick(e.srcElement);
+        });
+      }
+
+      setLocalStatus("");
+    }
+  };
+  xhr.send("action=list");
+}
+
+function handleSaveCallback() {
+  if (isXhrSuccess(xhr)) {
+    setRemoteStatus("");
+
+    savingFileOffset += blockSize;
+    if (savingFileOffset < savingText.length) {
+      setLocalStatus("<span class=\"icon icon-loading\"></span> Saving file: " + savingFilename + " " + savingFileOffset + "/" + savingText.length + " bytes");
+
+      var params = "action=append&filename=" + savingFilename + "&data=" + encodeURIComponent(savingText.substring(savingFileOffset, savingFileOffset + blockSize));
+      if (!xhr) xhr = new XMLHttpRequest();
+      xhr.open("POST", "file-api.lc", true);
+      xhr.onreadystatechange = handleSaveCallback;
+      xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+      xhr.send(params);
+    } else {
+      if ((savingFilename.split(".").pop() == "lua") && (savingFilename != "config.lua") && (savingFilename != "config.lua")) {
+        setLocalStatus("<span class=\"icon icon-loading\"></span> Compiling file: " + savingFilename);
+
+        params = "action=compile&filename=" + savingFilename;
+        if (!xhr) xhr = new XMLHttpRequest();
+        xhr.open("POST", "file-api.lc", true);
+        xhr.onreadystatechange = handleCompileCallback;
+        xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+        xhr.send(params);
+      } else {
+        setLocalStatus("Saved file: " + savingFilename + " " + savingText.length + " bytes");
+      }
+    }
+  } else {
+    setRemoteStatus("");
+    setRemoteStatus(xhr.responseText);
+  }
+}
+
+function handleCompileCallback() {
+  if (isXhrSuccess(xhr)) {
+    setLocalStatus("Compiled file: " + savingFilename);
+    setRemoteStatus("");
+  }
+}
+
+function loadFile() {
+  var filename = curFileItem.id;
+  setLocalStatus("Loading: " + filename);
+  setLocalStatus("<span class=\"icon icon-loading\"></span> Loading file: " + filename);
+
+  var params = "action=load&filename=" + filename;
+  if (!xhr) xhr = new XMLHttpRequest();
+  xhr.open("POST", "file-api.lc", true);
+  xhr.onreadystatechange = function () {
+    if (isXhrSuccess(xhr)) {
+      editor.setValue(xhr.responseText);
+      var extension = filename.split(".").pop();
+      switch (extension) {
+        case "css":
+          editor.setOption("mode", "css");
+          break;
+        case "htm":
+        case "html":
+          editor.setOption("mode", "htmlmixed");
+          break;
+        case "js":
+        case "json":
+          editor.setOption("mode", "javascript");
+          break;
+        case "lua":
+          editor.setOption("mode", "lua");
+          break;
+        case "xml":
+          editor.setOption("mode", "xml");
+          break;
+      }
+
+      setLocalStatus("");
+    }
+  }
+  xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+  xhr.send(params);
+}
+
+function undo() {
+  editor.undo();
+}
+
+function save() {
+  savingText = editor.getValue();
+  savingFilename = curFileItem.id;
+  savingFileOffset = 0;
+  setLocalStatus("<span class=\"icon icon-loading\"></span> Saving file: " + savingFilename + " " + savingFileOffset + "/" + savingText.length + " bytes");
+  setRemoteStatus("");
+  var params = "action=save&filename=" + savingFilename + "&data=" + encodeURIComponent(savingText.substring(savingFileOffset, savingFileOffset + blockSize));
+  if (!xhr) xhr = new XMLHttpRequest();
+  xhr.open("POST", "file-api.lc", true);
+  xhr.onreadystatechange = handleSaveCallback;
+  xhr.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
+  xhr.send(params);
+}
+
+function preview() {
+  if (curFileItem) {
+    var url = curFileItem.id;
+    var win = window.open(url, '_blank');
+    win.focus();
+  }
+}
+
+function init() {
+  editor = CodeMirror(document.getElementById("editor"), {
+    styleActiveLine: true, // activeline
+    lineNumbers: true,
+    lineWrapping: true,
+    autoCloseBrackets: true, // closebrackets
+    autoCloseTags: true, //closetag
+    gutters: ["CodeMirror-lint-markers"],
+    lint: true
+  });
+
+  loadFilelist();
+
+  document.getElementById("undo").addEventListener("click", undo);
+  document.getElementById("save").addEventListener("click", save);
+  document.getElementById("preview").addEventListener("click", preview);
+}
+
+// load large size script in sequence to avoid NodeMCU overflow
+// CodeMirror Compression helper https://codemirror.net/doc/compress.html
+// codemirror.js, css.js, htmlmixed.js, javascript.js, lua.js, xml.js
+// active-line.js, css-hint.js, html-hint.js, javascript-hint.js, trailingspace.js, xml-hint.js
+loadScript("codemirror-compressed.js", function () {
+  //loadScript("further-script.js", function () {
+  init();
+  //})
+})

Some files were not shown because too many files changed in this diff