summaryrefslogtreecommitdiff
path: root/graphics/figput/javascript/server.py
blob: 4974d3f88ce890ad82c4cc8bc36ecd48ce407010 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
"""
Use this to serve the document as it is being edited. It does several things
that no normal server does. It modifies the one and only HTML file before it
is passed to the client so that the correct document is opened. It will serve
files from directories outside the server directory. This way, the user can
keep his latex files in one place and this framework stuff in a different
directory. Also, the client can send .tikz files to the server and they
will be saved in the latex directory. Some of this (maybe all?) could be done
by taking an off-the-shelf server and tweaking it, but I want to keep things
simple, both for me and for the user. Another non-standard thing is that it
handles "WHERE" requests from the browswer; these are used so that that 
documents can be loaded to open so that the are scrolled to a certain position.

To invoke this, say

python server.py directory/nameof.pdf port_num

where directory/nameof.pdf is the pdf file we want to load. If the server
stuff is in top_level/something/figput_guts/ and the latex stuff is in 
top_level/elsewhere/latex/ then, after cd-ing to 
top_level/something/figput_guts/ you need to say

python server.py ../../elsewhere/latex/thefile.pdf 8080

where the 8080 is optional, and will default to 8000. Allowing for an 
optional port number permits the user to work on several documents at once.

BUG: It is tempting to write the entire thing in a language where 
I can compile it to something that uses no outside resources. Asking 
the user to install external stuff is not ideal, even if it's only
Python. I suspect that this could be done most easily in Rust, although
C/C++ would work, or most anything that compiles to an executable. It
just needs to be easily compiled for mac/linux/windows without any
contortions.

"""

import sys
import os
import shutil
from http.server import BaseHTTPRequestHandler, HTTPServer
 
 
# By default, documents open at the top of the first page, but this
# can be used (see do_WHERE()) to change that.
verticalScrollPos = 0


# BaseHTTPRequestHandler routes requests based on the text that appears in the
# request. So "POST" is routed to do_POST(), "GET" is routed to doGET(), and
# it would be trivial to add my own request type: "WACKY" would automatically
# be routed to do_WACKY(). That's exactly wheat is done below for the WHERE
# request.

class MyServer(BaseHTTPRequestHandler):
    
    def send_head(self):
        # Open and return the relevant file, or return None.
        
        # For whatever reason, the path that comes in always starts with a '/',
        # probably to emphasize that it's relative to the "root" of the server.
        # I break that concept.
        #
        # It is tempting to have the client reqest "draw_whatever" instead
        # of "draw_whatever.js" or "draw_whatever.fjs" I could then look for
        # both types here, saving a bit of effort on the js end. However, no
        # normal server does this, so I would still need to maintain a version
        # of the js that does ask for both types for use in a typical production
        # situation.
        path = self.path[1:]
        f = None
        if (self.path == '/'):
            # Request for the one and only html file, corresponding to localhost:8000
            path = os.path.join(path,"figput.html")
        else:
            if (not os.path.exists(path)):
                # Try the latex directory. Strip off leading '/' and pre=pend latex path.
                path = self.path[1:]
                path = os.path.join(args.latex_dir,path)
        
        try:
            f = open(path, 'rb')
        except OSError:
            self.send_error(404, "File not found")
            return None
        
        # The MIME type of the file *could* be ignored since "text/html" always
        # seems to work, but it does generate distracting error messages in the 
        # broswer.
        ctype = ""
        base, ext = os.path.splitext(path)
        if (ext == ".js"):
          ctype = "text/javascript"
        elif (ext == ".fjs"):
          ctype = "text/javascript"
        elif (ext == ".html"):
          ctype = "text/html"
        else:
          ctype = "text/plain"
          
        self.send_response(200)
        self.send_header("Content-type", ctype)
        self.end_headers()
        return f
        
    def do_HEAD(self):
        # Probably never called in my situation, although everything this
        # does must also be done for GET, so no harm in handling it.
        f = self.send_head()
        if f:
            f.close()
    
    def do_GET(self):
        f = self.send_head()
        if f:
            try:
                if (self.path == '/'):
                    # Special case of the root html file; replace 'unknown'.
                    buf = f.read()
                    buf = buf.replace(b'unknown1',bytes(args.project_name,'utf-8') )
                    buf = buf.replace(b'unknowny',bytes(str(verticalScrollPos),'utf-8') )
                    self.wfile.write(buf)
                else:
                  shutil.copyfileobj(f, self.wfile)
            finally:
                f.close()
                
    def do_POST(self):
      # self.path will be '/whatever.tikz'. Strip off the leading '/' and
      # pre-pend the latex directory. This is where the content is saved.
      dpath = self.path[1:]
      dpath = os.path.join(args.latex_dir,dpath)
      text_output_file = open(dpath,"w")
      
      # Get the length of the incoming payload and read it all.
      postLength = int(self.headers.get('content-length'))
      testmsg = self.rfile.read(postLength)
      
      # This is semi-pointless, but testmsg is a series of bytes, and 
      # write() below is looking for a string.
      newmsg = testmsg.decode("ASCII",'ignore')
      
      ignore = text_output_file.write(newmsg)
      
      text_output_file.flush()
      text_output_file.close()
      self.send_response(200, "File saved")
      self.flush_headers()
    
    def do_WHERE(self):
      # This is a trick to allow the browser to maintain the postion within the
      # document when it is reloaded. It passes the current scroll position, which
      # is then fed back to it when the document is reloaded.
      #
      # self.path is ignored. The client will send 'bogus' or the like.
      # We want the contents of the message. Start off very much as for POST.
      print("Got " +self.path);
      postLength = int(self.headers.get('content-length'))
      testmsg = self.rfile.read(postLength)
      newmsg = testmsg.decode("ASCII",'ignore')
      
      # newmsg is the postion within the document, as a string.
      global verticalScrollPos 
      verticalScrollPos = float(newmsg)
      
 
def testWhetherPDF(fpath):
    
    if (not os.path.exists(fpath)):
        print("No such file!")
        sys.exit(0)
    
    ftype = os.path.splitext(os.path.basename(fpath))[-1]
    if (ftype.lower() != ".pdf"):
        print("Not a PDF file!")
        sys.exit(0);


import argparse

parser = argparse.ArgumentParser()
parser.add_argument('file_path',help='specify path to pdf generated by LaTeX')
parser.add_argument('port', action='store', default=8000, type=int,
                    nargs='?',
                    help='specify alternate port (default: 8000)')
args = parser.parse_args()

testWhetherPDF(args.file_path)

# Parse out the directory leading to the PDF. It's used to search for js files.
# BEWARE: This is used above, as a global variable.
args.latex_dir = os.path.dirname(os.path.abspath(args.file_path))

# Do the same thing to get the name of the pdf file, without the '.pdf'.
# This is ultimately passed to the FigPut code so that it knows which file to
# open as the .pdf and as the .aux.
args.project_name = os.path.splitext(os.path.basename(args.file_path))[0]

theServer = HTTPServer(("localhost", args.port), MyServer)
print(f"Server started for http://localhost:{args.port}")

try:
    theServer.serve_forever()
except KeyboardInterrupt:
    pass

theServer.server_close()
print("Http server stopped.")