Aegisub example: make version mismatch message more helpful.

-- This library is unlicensed under CC0

ASSIVersionCompat = 0x000200
InspectorVersion  = 0x000201

ffi = require( 'ffi' )
bit = require( 'bit' )

InspectorVersion_major = bit.rshift( ASSIVersionCompat, 16 )
InspectorVersion_minor = bit.band( bit.rshift( ASSIVersionCompat, 8 ), 0xFF )
InspectorVersion_patch = bit.band( ASSIVersionCompat, 0xFF )

ASSIVersionCompat_major = bit.rshift( ASSIVersionCompat, 16 )
ASSIVersionCompat_minor = bit.band( bit.rshift( ASSIVersionCompat, 8 ), 0xFF )
ASSIVersionCompat_patch = bit.band( ASSIVersionCompat, 0xFF )

ffi.cdef( [[
typedef struct {
	int x, y;
	unsigned int w, h;
} ASSI_Rect;

uint32_t    assi_getVersion( void );
const char* assi_getErrorString( void* );
void*       assi_init( int, int, const char*, const char* );
void        assi_changeResolution( void*, int, int );
void        assi_reloadFonts( void*, const char*, const char* );
int         assi_setHeader( void*, const char*, size_t );
int         assi_setScript( void*, const char*, size_t );
int         assi_calculateBounds( void*, ASSI_Rect*, const int32_t*, const uint32_t );
void        assi_cleanup( void* );
]] )

-- Figure out a better way to load this?
extension = ('OSX' == ffi.os and '.dylib' or 'Windows' == ffi.os and '.dll' or '.so')
libraryPath = "?user/automation/include/ASSInspector"
success, ASSInspector = pcall( ffi.load, aegisub.decode_path( libraryPath .. "/libASSInspector" .. extension ) )
if not success
	libraryPath = "?data/automation/include/ASSInspector"
	success, ASSInspector = pcall( ffi.load, aegisub.decode_path( libraryPath .. "/libASSInspector" .. extension ) )
	if not success
		error( "Could not load required libASSInspector library." )

-- Returns true if the versions are compatible and nil, "message" if
-- they aren't.
looseVersionCompare = ( ASSIVersion ) ->
	ASSIVersion_major = bit.rshift( ASSIVersion, 16 )
	ASSIVersion_minor = bit.band( bit.rshift( ASSIVersion, 8 ), 0xFF )
	ASSIVersion_patch = bit.band( ASSIVersion, 0xFF )

	if ASSIVersion_major > ASSIVersionCompat_major
		return nil, ("Inspector.moon library is too old. Must be v%d.x.x")\format ASSIVersionCompat_major
	elseif ASSIVersion_major < ASSIVersionCompat_major or ASSIVersion_minor < ASSIVersionCompat_minor
		return nil, ("libASSInspector library is too old. Must be v%d.%d.x compatible.")\format ASSIVersionCompat_major, ASSIVersionCompat_minor

	return true

-- Inspector is not actually a property of the class but a property of
-- the script. Since we don't need to worry about concurrency or
-- anything silly like that, we can initialize ASSInspector once and
-- keep that reference alive until Aegisub is closed or scripts are all
-- reloaded.
state = {
	inspector:        nil
	fontconfigConfig: nil
	fontDir:          nil
	resX:             nil
	resY:             nil
}

collectHeader = ( subtitles ) =>
	scriptHeader = {
		"[Script Info]"
	}

	-- These are the only header fields that actually affect the way ASS
	-- is rendered. I don't actually know if ScriptType matters.
	infoFields = {
		PlayResX:   true
		PlayResY:   true
		WrapStyle:  true
		ScriptType: true
		ScaledBorderAndShadow: true
	}

	styles = { }

	seenStyles = false
	resX = 640
	resY = 480

	-- This loop assumes the subtitle script is nicely organized in the
	-- order [Script Info] -> [V4+ Styles] -> [Events]. This isn't
	-- guaranteed for scripts floating around in the wild, but fortunately
	-- Aegisub 3.2 seems to do a very good job of ensuring this order when
	-- a script is loaded. In other words, this loop is probably only safe
	-- to do from within automation.
	for index = 1, #subtitles
		with line = subtitles[index]
			if "info" == .class
				if infoFields[.key]
					table.insert( scriptHeader, .raw )

				if "PlayResX" == .key
					resX = tonumber( .value )
				elseif "PlayResY" == .key
					resY = tonumber( .value )

			elseif "style" == .class
				unless seenStyles
					table.insert( scriptHeader, "[V4+ Styles]\n" )
					seenStyles = true

				styles[.name] = .raw

			elseif "dialogue" == .class
				break

	-- If a video is loaded, use its resolution instead of the script's
	-- resolution. Don't use low-resolution workraws for typesetting!
	vidResX, vidResY = aegisub.video_size( )
	@resX = vidResX or resX
	@resY = vidResY or resY

	@header = table.concat( scriptHeader, '\n' )
	@styles = styles

initializeInspector = ( fontconfigConfig = state.fontconfigConfig, fontDirectory = state.fontDir ) ->
	if nil == state.inspector
		success, message = looseVersionCompare( ASSInspector.assi_getVersion! )
		unless success
			return nil, message

		state.inspector = ffi.gc( ASSInspector.assi_init( 1, 1, fontconfigConfig, fontDirectory ), ASSInspector.assi_cleanup )

		if nil == state.inspector
			return nil, "ASSInspector library initialization failed."

		state.resX = 1
		state.resY = 1
		state.fontDir = fontDirectory
		state.fontconfigConfig = fontconfigConfig

	return true

validateRect = ( rect ) ->
	bounds = {
		x: tonumber( rect.x )
		y: tonumber( rect.y )
		w: tonumber( rect.w )
		h: tonumber( rect.h )
	}

	if bounds.w == 0 or bounds.h == 0
		return false
	else
		return bounds

defaultTimes = ( lines ) ->
	ffms = aegisub.frame_from_ms
	msff = aegisub.ms_from_frame

	seenTimes = { }
	times = { }
	hasFrames = ffms( 0 )

	if hasFrames
		for line in *lines
			with line
				for frame = ffms( .start_time ), true == .assi_exhaustive and ffms( .end_time ) - 1 or ffms( .start_time )
					frameTime = math.floor( 0.5*( msff( frame ) + msff( frame + 1 ) ) )
					unless seenTimes[frameTime]
						table.insert( times, frameTime )
						seenTimes[frameTime] = true
	else
		for line in *lines
			with line
				unless seenTimes[.start_time]
					table.insert( times, .start_time )
					seenTimes[.start_time] = true

	-- This will only happen if all lines are displayed for 0 frames.
	unless 0 < #times
		return false
	else
		return times

addStyles = ( line, scriptText, seenStyles ) =>
	if @styles[line.style] and not seenStyles[line.style]
		table.insert( scriptText, @styles[line.style] )
		seenStyles[line.style] = true

	line.text\gsub "{(.-)}", ( tagBlock ) ->
		tagBlock\gsub "\\r([^\\}]*)", ( styleName ) ->
			if 0 < #styleName and @styles[styleName] and not seenStyles[styleName]
				table.insert( scriptText, @styles[styleName] )
				seenStyles[styleName] = true

class Inspector
	@version = InspectorVersion
	@version_major = InspectorVersion_major
	@version_minor = InspectorVersion_minor
	@version_patch = InspectorVersion_patch

	new: ( subtitles, fontconfigConfig = aegisub.decode_path( libraryPath .. "/fonts.conf" ), fontDirectory = aegisub.decode_path( '?script/fonts' ) ) =>
		assert subtitles, "You must provide the subtitles object."

		-- Does nothing if inspector is already initialized. The initialized
		-- ASSInspector is stored at the script scope, which means it
		-- persists until either Aegisub quits, automation scripts are
		-- reloaded, or forceLibraryReload is called.
		success, message = initializeInspector( fontconfigConfig, fontDirectory )
		assert success, message

		success, message = @updateHeader( subtitles )
		assert success, message

	updateHeader: ( subtitles ) =>
		if nil == subtitles
			return nil, "You must provide the subtitles object."

		collectHeader( @, subtitles )

		if @resX != state.resX or @resY != state.resY
			state.resX, state.resY = @resX, @resY
			ASSInspector.assi_changeResolution( state.inspector, @resX, @resY )

		@reloadFonts!

		if 0 < ASSInspector.assi_setHeader( state.inspector, @header, #@header )
			return nil, "Failed to set header.\n" .. ffi.string( ASSInspector.assi_getErrorString( state.inspector ) )

		return true

	reloadFonts: ( fontconfigConfig = state.fontconfigConfig, fontDirectory = state.fontDir ) =>
		ASSInspector.assi_reloadFonts( state.inspector, fontconfigConfig, fontDirectory )

	forceLibraryReload: ( subtitles, fontconfigConfig, fontDirectory ) =>
		if nil == subtitles
			return nil, "You must provide the subtitles object."

		state.inspector = nil
		-- let initializeInspector handle the default arguments.
		success, message = initializeInspector( fontconfigConfig, fontDirectory )
		if nil == success
			return success, message

		@updateHeader( subtitles )

	-- Arguments:
	-- subtitles: the subtitles object that Aegisub passes to the macro.

	-- line: an array-like table of line tables that have at least the
	--       fields `start_time`, `text`, `end_time` and `raw`. `raw` is
	--       mandatory in all cases. `start_time` and `end_time` are only
	--       necessary if a table of times to render the line is not
	--       provided and a video is loaded. Allows rendering 1 or more
	--       lines simultaneously.

	-- times: a table of times (in milliseconds) to render the line at.
	--        Defaults to the start time of the line unless there is a video
	--        loaded and the line contains the field line.assi_exhaustive =
	--        true, in which case it defaults to every single frame that the
	--        line is displayed.

	-- Returns:
	-- An array-like table of bounding boxes, and an array-like table of
	-- render times. The two tables are the same length. If multiple lines
	-- are passed, the bounding box for a given time is the combined
	-- bounding boxes of all the lines rendered at that time.

	-- Error Handling:
	-- If an error is encountered, getBounds returns nil and an error string,
	-- which is typical for lua error reporting. In order to distinguish
	-- between an error and a valid false return, users should make sure
	-- they actually compare result to nil and false, rather than just
	-- checking that the result is not falsy.

	getBounds: ( lines, times = defaultTimes( lines ) ) =>
		unless times
			return nil, "The render times table was empty."

		for i = 1,#lines
			line = lines[i]
			if nil == line.raw
				if line.createRaw
					line\createRaw!
				else
					return nil, "line.raw is missing from line #{i}."

		scriptText = { }
		seenStyles = { }
		if @styles.Default
			seenStyles.Default = true
			table.insert( scriptText, @styles.Default )

		for line in *lines
			addStyles( @, line, scriptText, seenStyles )

		table.insert( scriptText, '[Events]' )

		for line in *lines
			table.insert( scriptText, line.raw )

		scriptString = table.concat( scriptText, '\n' )
		if 0 < ASSInspector.assi_setScript( state.inspector, scriptString, #scriptString )
			return nil, "Could not set script" .. ffi.string( ASSInspector.assi_getErrorString( state.inspector ) )

		renderCount = #times
		cTimes = ffi.new( 'int32_t[?]', renderCount )
		cRects = ffi.new( 'ASSI_Rect[?]', renderCount )

		for i = 0, renderCount - 1
			cTimes[i] = times[i + 1]

		if 0 < ASSInspector.assi_calculateBounds( state.inspector, cRects, cTimes, renderCount )
			return nil, "Error calculating bounds" .. ffi.string( ASSInspector.assi_getErrorString( state.inspector ) )

		rects = { }
		for i = 0, renderCount - 1
			table.insert( rects, validateRect( cRects[i] ) )

		return rects, times

return Inspector

-- This library is unlicensed under CC0

ASSIVersionCompat = 0x000200
InspectorVersion  = 0x000201

ffi = require( 'ffi' )
bit = require( 'bit' )

ASSIVersionCompat_major = bit.rshift( ASSIVersionCompat, 16 )
ASSIVersionCompat_minor = bit.band( bit.rshift( ASSIVersionCompat, 8 ), 0xFF )
ASSIVersionCompat_patch = bit.band( ASSIVersionCompat, 0xFF )

ffi.cdef( [[
typedef struct {
	int x, y;
	unsigned int w, h;
} ASSI_Rect;

uint32_t    assi_getVersion( void );
const char* assi_getErrorString( void* );
void*       assi_init( int, int, const char*, const char* );
void        assi_changeResolution( void*, int, int );
void        assi_reloadFonts( void*, const char*, const char* );
int         assi_setHeader( void*, const char*, size_t );
int         assi_setScript( void*, const char*, size_t );
int         assi_calculateBounds( void*, ASSI_Rect*, const int32_t*, const uint32_t );
void        assi_cleanup( void* );
]] )

-- Figure out a better way to load this?
extension = ('OSX' == ffi.os and '.dylib' or 'Windows' == ffi.os and '.dll' or '.so')
libraryPath = "?user/automation/include/ASSInspector"
success, ASSInspector = pcall( ffi.load, aegisub.decode_path( libraryPath .. "/libASSInspector" .. extension ) )
if not success
	libraryPath = "?data/automation/include/ASSInspector"
	success, ASSInspector = pcall( ffi.load, aegisub.decode_path( libraryPath .. "/libASSInspector" .. extension ) )
	if not success
		error( "Could not load required libASSInspector library." )

-- Returns true if the versions are compatible and nil, "message" if
-- they aren't.
looseVersionCompare = ( ASSIVersion ) ->
	ASSIVersion_major = bit.rshift( ASSIVersion, 16 )
	ASSIVersion_minor = bit.band( bit.rshift( ASSIVersion, 8 ), 0xFF )
	ASSIVersion_patch = bit.band( ASSIVersion, 0xFF )

	if ASSIVersion_major > ASSIVersionCompat_major
		return nil, "Inspector.moon library is too old."
	elseif ASSIVersion_major < ASSIVersionCompat_major or ASSIVersion_minor < ASSIVersionCompat_minor
		return nil, "libASSInspector library is too old."

	return true

-- Inspector is not actually a property of the class but a property of
-- the script. Since we don't need to worry about concurrency or
-- anything silly like that, we can initialize ASSInspector once and
-- keep that reference alive until Aegisub is closed or scripts are all
-- reloaded.
state = {
	inspector:        nil
	fontconfigConfig: nil
	fontDir:          nil
	resX:             nil
	resY:             nil
}

collectHeader = ( subtitles ) =>
	scriptHeader = {
		"[Script Info]"
	}

	-- These are the only header fields that actually affect the way ASS
	-- is rendered. I don't actually know if ScriptType matters.
	infoFields = {
		PlayResX:   true
		PlayResY:   true
		WrapStyle:  true
		ScriptType: true
		ScaledBorderAndShadow: true
	}

	styles = { }

	seenStyles = false
	resX = 640
	resY = 480

	-- This loop assumes the subtitle script is nicely organized in the
	-- order [Script Info] -> [V4+ Styles] -> [Events]. This isn't
	-- guaranteed for scripts floating around in the wild, but fortunately
	-- Aegisub 3.2 seems to do a very good job of ensuring this order when
	-- a script is loaded. In other words, this loop is probably only safe
	-- to do from within automation.
	for index = 1, #subtitles
		with line = subtitles[index]
			if "info" == .class
				if infoFields[.key]
					table.insert( scriptHeader, .raw )

				if "PlayResX" == .key
					resX = tonumber( .value )
				elseif "PlayResY" == .key
					resY = tonumber( .value )

			elseif "style" == .class
				unless seenStyles
					table.insert( scriptHeader, "[V4+ Styles]\n" )
					seenStyles = true

				styles[.name] = .raw

			elseif "dialogue" == .class
				break

	-- If a video is loaded, use its resolution instead of the script's
	-- resolution. Don't use low-resolution workraws for typesetting!
	vidResX, vidResY = aegisub.video_size( )
	@resX = vidResX or resX
	@resY = vidResY or resY

	@header = table.concat( scriptHeader, '\n' )
	@styles = styles

initializeInspector = ( fontconfigConfig = state.fontconfigConfig, fontDirectory = state.fontDir ) ->
	if nil == state.inspector
		success, message = looseVersionCompare( ASSInspector.assi_getVersion! )
		unless success
			return nil, message

		state.inspector = ffi.gc( ASSInspector.assi_init( 1, 1, fontconfigConfig, fontDirectory ), ASSInspector.assi_cleanup )

		if nil == state.inspector
			return nil, "ASSInspector library initialization failed."

		state.resX = 1
		state.resY = 1
		state.fontDir = fontDirectory
		state.fontconfigConfig = fontconfigConfig

	return true

validateRect = ( rect ) ->
	bounds = {
		x: tonumber( rect.x )
		y: tonumber( rect.y )
		w: tonumber( rect.w )
		h: tonumber( rect.h )
	}

	if bounds.w == 0 or bounds.h == 0
		return false
	else
		return bounds

defaultTimes = ( lines ) ->
	ffms = aegisub.frame_from_ms
	msff = aegisub.ms_from_frame

	seenTimes = { }
	times = { }
	hasFrames = ffms( 0 )

	if hasFrames
		for line in *lines
			with line
				for frame = ffms( .start_time ), true == .assi_exhaustive and ffms( .end_time ) - 1 or ffms( .start_time )
					frameTime = math.floor( 0.5*( msff( frame ) + msff( frame + 1 ) ) )
					unless seenTimes[frameTime]
						table.insert( times, frameTime )
						seenTimes[frameTime] = true
	else
		for line in *lines
			with line
				unless seenTimes[.start_time]
					table.insert( times, .start_time )
					seenTimes[.start_time] = true

	-- This will only happen if all lines are displayed for 0 frames.
	unless 0 < #times
		return false
	else
		return times

addStyles = ( line, scriptText, seenStyles ) =>
	if @styles[line.style] and not seenStyles[line.style]
		table.insert( scriptText, @styles[line.style] )
		seenStyles[line.style] = true

	line.text\gsub "{(.-)}", ( tagBlock ) ->
		tagBlock\gsub "\\r([^\\}]*)", ( styleName ) ->
			if 0 < #styleName and @styles[styleName] and not seenStyles[styleName]
				table.insert( scriptText, @styles[styleName] )
				seenStyles[styleName] = true

class Inspector
	@version = InspectorVersion
	@version_major = bit.rshift( ASSIVersionCompat, 16 )
	@version_minor = bit.band( bit.rshift( ASSIVersionCompat, 8 ), 0xFF )
	@version_patch = bit.band( ASSIVersionCompat, 0xFF )

	new: ( subtitles, fontconfigConfig = aegisub.decode_path( libraryPath .. "/fonts.conf" ), fontDirectory = aegisub.decode_path( '?script/fonts' ) ) =>
		assert subtitles, "You must provide the subtitles object."

		-- Does nothing if inspector is already initialized. The initialized
		-- ASSInspector is stored at the script scope, which means it
		-- persists until either Aegisub quits, automation scripts are
		-- reloaded, or forceLibraryReload is called.
		success, message = initializeInspector( fontconfigConfig, fontDirectory )
		assert success, message

		success, message = @updateHeader( subtitles )
		assert success, message

	updateHeader: ( subtitles ) =>
		if nil == subtitles
			return nil, "You must provide the subtitles object."

		collectHeader( @, subtitles )

		if @resX != state.resX or @resY != state.resY
			state.resX, state.resY = @resX, @resY
			ASSInspector.assi_changeResolution( state.inspector, @resX, @resY )

		@reloadFonts!

		if 0 < ASSInspector.assi_setHeader( state.inspector, @header, #@header )
			return nil, "Failed to set header.\n" .. ffi.string( ASSInspector.assi_getErrorString( state.inspector ) )

		return true

	reloadFonts: ( fontconfigConfig = state.fontconfigConfig, fontDirectory = state.fontDir ) =>
		ASSInspector.assi_reloadFonts( state.inspector, fontconfigConfig, fontDirectory )

	forceLibraryReload: ( subtitles, fontconfigConfig, fontDirectory ) =>
		if nil == subtitles
			return nil, "You must provide the subtitles object."

		state.inspector = nil
		-- let initializeInspector handle the default arguments.
		success, message = initializeInspector( fontconfigConfig, fontDirectory )
		if nil == success
			return success, message

		@updateHeader( subtitles )

	-- Arguments:
	-- subtitles: the subtitles object that Aegisub passes to the macro.

	-- line: an array-like table of line tables that have at least the
	--       fields `start_time`, `text`, `end_time` and `raw`. `raw` is
	--       mandatory in all cases. `start_time` and `end_time` are only
	--       necessary if a table of times to render the line is not
	--       provided and a video is loaded. Allows rendering 1 or more
	--       lines simultaneously.

	-- times: a table of times (in milliseconds) to render the line at.
	--        Defaults to the start time of the line unless there is a video
	--        loaded and the line contains the field line.assi_exhaustive =
	--        true, in which case it defaults to every single frame that the
	--        line is displayed.

	-- Returns:
	-- An array-like table of bounding boxes, and an array-like table of
	-- render times. The two tables are the same length. If multiple lines
	-- are passed, the bounding box for a given time is the combined
	-- bounding boxes of all the lines rendered at that time.

	-- Error Handling:
	-- If an error is encountered, getBounds returns nil and an error string,
	-- which is typical for lua error reporting. In order to distinguish
	-- between an error and a valid false return, users should make sure
	-- they actually compare result to nil and false, rather than just
	-- checking that the result is not falsy.

	getBounds: ( lines, times = defaultTimes( lines ) ) =>
		unless times
			return nil, "The render times table was empty."

		for i = 1,#lines
			line = lines[i]
			if nil == line.raw
				if line.createRaw
					line\createRaw!
				else
					return nil, "line.raw is missing from line #{i}."

		scriptText = { }
		seenStyles = { }
		if @styles.Default
			seenStyles.Default = true
			table.insert( scriptText, @styles.Default )

		for line in *lines
			addStyles( @, line, scriptText, seenStyles )

		table.insert( scriptText, '[Events]' )

		for line in *lines
			table.insert( scriptText, line.raw )

		scriptString = table.concat( scriptText, '\n' )
		if 0 < ASSInspector.assi_setScript( state.inspector, scriptString, #scriptString )
			return nil, "Could not set script" .. ffi.string( ASSInspector.assi_getErrorString( state.inspector ) )

		renderCount = #times
		cTimes = ffi.new( 'int32_t[?]', renderCount )
		cRects = ffi.new( 'ASSI_Rect[?]', renderCount )

		for i = 0, renderCount - 1
			cTimes[i] = times[i + 1]

		if 0 < ASSInspector.assi_calculateBounds( state.inspector, cRects, cTimes, renderCount )
			return nil, "Error calculating bounds" .. ffi.string( ASSInspector.assi_getErrorString( state.inspector ) )

		rects = { }
		for i = 0, renderCount - 1
			table.insert( rects, validateRect( cRects[i] ) )

		return rects, times

return Inspector


ircsnap: use target in <title>

ircsnap: use target in <title>

Former-commit-id: ffcc7b4c931c78463ba38405831be836159b813a
Former-commit-id: a5bf1e88eb0a353dee37966c48cf9f2e9256999f

{:urlEncode} = require'util'
html2unicode = require 'html'
nixio = require'nixio'
fs = require'nixio.fs'
ltn12 = require'ltn12'

-- All URLs in this module is under this prefix
urlbase = '/image/'

safe = (fn) ->
  f, ext = fn\match'^(.*)%.(.-)$'
  f = f\gsub '[^%w%-]', ''
  return f..'.'..ext

video_html = (video) ->
  videourl = ivar2.config.webserverprefix..urlbase..'file/'..video
  [[
  <!DOCTYPE html>

  <meta charset="utf-8">

    <video id="v" src="]]..videourl..[[" controls autoplay loop>
      Your browser does not support the video element.
      Try the direct link to the video: video
    </video>

    <button onclick="rotate(90)">Rotate ⟳</button>
    <button onclick="rotate(-90)">Rotate ⟲</button>

  ]]

ivar2.webserver.regUrl "#{urlbase}(.*)$", (req, res) ->
  send = (body, code, content_type) ->
    if not code then code = 200
    if not content_type then content_type = 'text/html'
    res\set_status code
    res\set_header 'Content-Type', content_type
    res\set_header 'Content-Length', #body
    res\set_body body
    res\send!
    res.on_error = (res, req, err) ->
      ivar2\Log 'error', 'imageupload: error: %s', err

  file = req.url\match '/file/(.*)$'
  if file
    fn = "cache/images/#{safe file}"
    size = nixio.fs.stat(fn).size
    fd = ltn12.source.file(io.open fn, 'rb')
    --body = fd\read '*a'
    --fd\close!
    content_type = 'image/jpeg'
    if file\lower!\match '.png'
      content_type = 'image/png'
    if file\lower!\match '.svg'
      content_type = 'image/svg'
    if file\lower!\match '.mp4'
      content_type = 'video/mp4'
    if file\lower!\match '.mov'
      content_type = 'video/quicktime'
    if file\lower!\match '.mp3'
      content_type = 'audio/mp3'

    res\set_status code
    res\set_header 'Content-Type', content_type
    res\set_header 'Content-Length', size
    res\set_body fd
    res\send!
    ivar2\Log 'error', 'imageupload: serving: %s', fn
    return

  -- Serve video player page
  video = req.url\match '/video/(.*)$'
  if video then
    return send video_html(video)

  channel = req.url\match('channel=(.+)%s*')
  unless channel
    send 'Invalid channel', 404
    return

  channel = html2unicode channel
  unescaped_channel = channel\gsub '%%23', '#'

  html = [[
  <!DOCTYPE html>

  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=Edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
  <meta name="mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black">
  <meta name="apple-mobile-web-app-title" content="IRCSNAP ]]..unescaped_channel..[[">
  <meta name="theme-color" content="#8f4099">

  <title>IRCSNAP ]]..unescaped_channel..[[</title>

Share to IRC app - ]]..unescaped_channel..[[ edition

Fill in your nick/name and optional text and then click on one of the yellow buttons to attach image. It will appear instantly on IRC!

    <form>

        <input id="sender" type="text" required>

        Your nickname

        <input id="text" type="text" required>

        Attach text, if you want

    </form>

Uploading. Percent complete: 0 %

      📷 Snap picture! <input type="file" accept="image/*" id="capturei" capture="camera" style="visibility:hidden;">
      📂 Browse gallery! <input type="file" accept="image/*" id="capturef" style="visibility:hidden;">
      🎥 Capture video! <input type="file" accept="video/*" capture="camcorder" id="capturev" style="visibility:hidden;">
      🎥 Upload video! <input type="file" accept="video/*" id="capturevf" style="visibility:hidden;">

      What is this?

Share to IRC is a simple web app for sharing media directly from your device to an IRC channel.

Click Tools ->  Add to homescreen to create a convenient shortcut for easy and fast access to sharing

Made by xt

  ]]
  if req.method == 'POST'
    fn = req.headers['X-Filename']
    sender = req.headers['X-Sender'] or ''
    text = req.headers['X-Text'] or ''
    file = req.filename
    ivar2\Log 'info', "imageupload: Recieved file name: <#{fn}> datalen: #{#file}, sender: <#{sender}> text: <#{text}>, channel: <#{unescaped_channel}>"
    if fn and file
      html = 'Ok'
      realfn = "#{os.time!}-#{safe fn}"
      save = ->
        --fd = nixio.open "cache/images/#{realfn}", 'w+', 0400
        --written = fd\write file
        --fd\sync!
        --fd\close!
        -- Move tempfile to real location
        fs.move req.filename, "cache/images/#{realfn}"
        if sender ~= ''
          sender = "<#{sender\sub(1, 100)}> "
        if text ~= ''
          text = text\sub(1, 100) .. ' '
        file_or_video = 'file'
        if realfn\match '%.mp4$'
          file_or_video = 'video'
        msg = "[IRCSNAP] #{sender}#{text}#{ivar2.config.webserverprefix}#{urlbase}#{file_or_video}/#{realfn}"
        ivar2\Privmsg unescaped_channel, msg
      ok, err = pcall(save)
      unless ok
        ivar2\Log 'error', "imageupload: Error during saving upload: %s", err
    else
      html = 'Not OK'

  send html

-- Attempt to create the cache folder.
nixio.fs.mkdir('cache/images')

PRIVMSG:
  '^%pircsnap$': (source, destination) =>
    channel = urlEncode destination
    say "#{ivar2.config.webserverprefix}#{urlbase}?channel=#{channel} IRCSNAP - sharing is caring."

{:urlEncode} = require'util'
html2unicode = require 'html'
nixio = require'nixio'
fs = require'nixio.fs'
ltn12 = require'ltn12'

-- All URLs in this module is under this prefix
urlbase = '/image/'

safe = (fn) ->
  f, ext = fn\match'^(.*)%.(.-)$'
  f = f\gsub '[^%w%-]', ''
  return f..'.'..ext

video_html = (video) ->
  videourl = ivar2.config.webserverprefix..urlbase..'file/'..video
  [[
  <!DOCTYPE html>

  <meta charset="utf-8">

    <video id="v" src="]]..videourl..[[" controls autoplay loop>
      Your browser does not support the video element.
      Try the direct link to the video: video
    </video>

    <button onclick="rotate(90)">Rotate ⟳</button>
    <button onclick="rotate(-90)">Rotate ⟲</button>

  ]]

ivar2.webserver.regUrl "#{urlbase}(.*)$", (req, res) ->
  send = (body, code, content_type) ->
    if not code then code = 200
    if not content_type then content_type = 'text/html'
    res\set_status code
    res\set_header 'Content-Type', content_type
    res\set_header 'Content-Length', #body
    res\set_body body
    res\send!
    res.on_error = (res, req, err) ->
      ivar2\Log 'error', 'imageupload: error: %s', err

  file = req.url\match '/file/(.*)$'
  if file
    fn = "cache/images/#{safe file}"
    size = nixio.fs.stat(fn).size
    fd = ltn12.source.file(io.open fn, 'rb')
    --body = fd\read '*a'
    --fd\close!
    content_type = 'image/jpeg'
    if file\lower!\match '.png'
      content_type = 'image/png'
    if file\lower!\match '.svg'
      content_type = 'image/svg'
    if file\lower!\match '.mp4'
      content_type = 'video/mp4'
    if file\lower!\match '.mov'
      content_type = 'video/quicktime'
    if file\lower!\match '.mp3'
      content_type = 'audio/mp3'

    res\set_status code
    res\set_header 'Content-Type', content_type
    res\set_header 'Content-Length', size
    res\set_body fd
    res\send!
    ivar2\Log 'error', 'imageupload: serving: %s', fn
    return

  -- Serve video player page
  video = req.url\match '/video/(.*)$'
  if video then
    return send video_html(video)

  channel = req.url\match('channel=(.+)%s*')
  unless channel
    send 'Invalid channel', 404
    return

  channel = html2unicode channel
  unescaped_channel = channel\gsub '%%23', '#'

  html = [[
  <!DOCTYPE html>

  <meta charset="utf-8">
  <meta http-equiv="X-UA-Compatible" content="IE=Edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">
  <meta name="mobile-web-app-capable" content="yes">
  <meta name="apple-mobile-web-app-status-bar-style" content="black">
  <meta name="apple-mobile-web-app-title" content="IRCSNAP ]]..unescaped_channel..[[">
  <meta name="theme-color" content="#8f4099">

  <title>IRCSNAP</title>

Share to IRC app - ]]..unescaped_channel..[[ edition

Fill in your nick/name and optional text and then click on one of the yellow buttons to attach image. It will appear instantly on IRC!

    <form>

        <input id="sender" type="text" required>

        Your nickname

        <input id="text" type="text" required>

        Attach text, if you want

    </form>

Uploading. Percent complete: 0 %

      📷 Snap picture! <input type="file" accept="image/*" id="capturei" capture="camera" style="visibility:hidden;">
      📂 Browse gallery! <input type="file" accept="image/*" id="capturef" style="visibility:hidden;">
      🎥 Capture video! <input type="file" accept="video/*" capture="camcorder" id="capturev" style="visibility:hidden;">
      🎥 Upload video! <input type="file" accept="video/*" id="capturevf" style="visibility:hidden;">

      What is this?

Share to IRC is a simple web app for sharing media directly from your device to an IRC channel.

Click Tools ->  Add to homescreen to create a convenient shortcut for easy and fast access to sharing

Made by xt

  ]]
  if req.method == 'POST'
    fn = req.headers['X-Filename']
    sender = req.headers['X-Sender'] or ''
    text = req.headers['X-Text'] or ''
    file = req.filename
    ivar2\Log 'info', "imageupload: Recieved file name: <#{fn}> datalen: #{#file}, sender: <#{sender}> text: <#{text}>, channel: <#{unescaped_channel}>"
    if fn and file
      html = 'Ok'
      realfn = "#{os.time!}-#{safe fn}"
      save = ->
        --fd = nixio.open "cache/images/#{realfn}", 'w+', 0400
        --written = fd\write file
        --fd\sync!
        --fd\close!
        -- Move tempfile to real location
        fs.move req.filename, "cache/images/#{realfn}"
        if sender ~= ''
          sender = "<#{sender\sub(1, 100)}> "
        if text ~= ''
          text = text\sub(1, 100) .. ' '
        file_or_video = 'file'
        if realfn\match '%.mp4$'
          file_or_video = 'video'
        msg = "[IRCSNAP] #{sender}#{text}#{ivar2.config.webserverprefix}#{urlbase}#{file_or_video}/#{realfn}"
        ivar2\Privmsg unescaped_channel, msg
      ok, err = pcall(save)
      unless ok
        ivar2\Log 'error', "imageupload: Error during saving upload: %s", err
    else
      html = 'Not OK'

  send html

-- Attempt to create the cache folder.
nixio.fs.mkdir('cache/images')

PRIVMSG:
  '^%pircsnap$': (source, destination) =>
    channel = urlEncode destination
    say "#{ivar2.config.webserverprefix}#{urlbase}?channel=#{channel} IRCSNAP - sharing is caring."


youtube: also delete last id when unsubscribing

Former-commit-id: f0521f3c11013fa3cbce63d72ce6f05f8ae3a217 [formerly 23ecc066d947c052e120335fe580751efa1306ea]
Former-commit-id: 6df67c43a6960e1b037d4627a4ac6a1bb3c17092

-- Module for announcing new videos to a youtube channel
-- Requires a developer API key set in configuration
{:simplehttp, :json, :urlEncode, :bold} = require'util'

moduleName = 'youtube'
key = moduleName
store = ivar2.persist

getItems = (channel, cb) ->
  url = "https://www.googleapis.com/youtube/v3/channels?part=contentDetails&forUsername=%s&key=%s"
  simplehttp url\format(urlEncode(channel), ivar2.config.youtubeAPIKey), (data) ->
    js = json.decode(data)
    if not (js and js.items and js.items[1] and js.items[1].contentDetails)
      ivar2\Log 'info', 'youtube: No items for youtube channel '..tostring(channel)
      return
    playlistId = js.items[1].contentDetails.relatedPlaylists.uploads
    if playlistId
      url = "https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&playlistId=%s&key=%s"
      simplehttp url\format(playlistId, ivar2.config.youtubeAPIKey), (pldata) ->
        pljs = json.decode(pldata)
        cb(pljs.items)

getLatest = (source, destination, channel) =>
  getItems channel, (items) ->
    for item in *items
      if item.kind == "youtube#playlistItem"
        s = item.snippet
        videoId = s.resourceId.videoId
        say "[#{bold s.channelTitle}] http://youtu.be/#{videoId} #{s.title}"
        return

checkChannels = ->
  for c,_ in pairs(ivar2.channels)
    channelKey = key..':'..c
    channels = store[channelKey] or {}
    for name, channel in pairs(channels)
      lastKey = channelKey .. ':' .. name .. ':last'
      last = store[lastKey] or ''
      getItems name, (items) ->
        for item in *items
          if item.kind == "youtube#playlistItem"
            s = item.snippet
            videoId = s.resourceId.videoId
            -- Check if we already announced
            if videoId == last
              return
            -- Store first video in playlist as a marker
            if s.position == 0
              store[lastKey] = videoId
            ivar2\Msg 'privmsg', channel.channel, nil, "[#{bold s.channelTitle}] http://youtu.be/#{videoId} — #{s.title}"
            -- If we're newly subscribed, only tell once
            if last == ''
              return

subscribe = (source, destination, name) =>
  channelKey = key..':'..destination
  channels = store[channelKey] or {}
  channels[name] = {channel:destination, name:name}
  store[channelKey] = channels
  reply "Ok. Subscribed to #{bold name}"
  checkChannels()

unsubscribe = (source, destination, name) =>
  channelKey = key..':'..destination
  channels = store[channelKey] or {}
  unless channels[name]
    reply "Wasn't subscribed. But, sure."
  else
    channels[name] = nil
    store[channelKey] = channels
    lastKey = channelKey .. ':' .. name .. ':last'
    store[lastKey] = nil
    reply "Ok. Stopped caring about #{bold name}"

list = (source, destination) =>
  channelKey = key..':'..destination
  channels = store[channelKey] or {}
  out = {}
  for name, game in pairs(channels)
      out[#out+1] = name
  say "Subscribed to: #{table.concat(out, ', ')}"

-- Start the subscribe poller
ivar2\Timer('youtube', 300, 300, checkChannels)

PRIVMSG:
  '^%pyoutube latest (.*)': getLatest
  '^%pyoutube subscribe (.*)': subscribe
  '^%pyoutube unsubscribe (.*)': unsubscribe
  '^%pyoutube list': list

-- Module for announcing new videos to a youtube channel
-- Requires a developer API key set in configuration
{:simplehttp, :json, :urlEncode, :bold} = require'util'

moduleName = 'youtube'
key = moduleName
store = ivar2.persist

getItems = (channel, cb) ->
  url = "https://www.googleapis.com/youtube/v3/channels?part=contentDetails&forUsername=%s&key=%s"
  simplehttp url\format(urlEncode(channel), ivar2.config.youtubeAPIKey), (data) ->
    js = json.decode(data)
    if not (js and js.items and js.items[1] and js.items[1].contentDetails)
      ivar2\Log 'info', 'youtube: No items for youtube channel '..tostring(channel)
      return
    playlistId = js.items[1].contentDetails.relatedPlaylists.uploads
    if playlistId
      url = "https://www.googleapis.com/youtube/v3/playlistItems?part=snippet&playlistId=%s&key=%s"
      simplehttp url\format(playlistId, ivar2.config.youtubeAPIKey), (pldata) ->
        pljs = json.decode(pldata)
        cb(pljs.items)

getLatest = (source, destination, channel) =>
  getItems channel, (items) ->
    for item in *items
      if item.kind == "youtube#playlistItem"
        s = item.snippet
        videoId = s.resourceId.videoId
        say "[#{bold s.channelTitle}] http://youtu.be/#{videoId} #{s.title}"
        return

checkChannels = ->
  for c,_ in pairs(ivar2.channels)
    channelKey = key..':'..c
    channels = store[channelKey] or {}
    for name, channel in pairs(channels)
      lastKey = channelKey .. ':' .. name .. ':last'
      last = store[lastKey] or ''
      getItems name, (items) ->
        for item in *items
          if item.kind == "youtube#playlistItem"
            s = item.snippet
            videoId = s.resourceId.videoId
            -- Check if we already announced
            if videoId == last
              return
            -- Store first video in playlist as a marker
            if s.position == 0
              store[lastKey] = videoId
            ivar2\Msg 'privmsg', channel.channel, nil, "[#{bold s.channelTitle}] http://youtu.be/#{videoId} — #{s.title}"
            -- If we're newly subscribed, only tell once
            if last == ''
              return

subscribe = (source, destination, name) =>
  channelKey = key..':'..destination
  channels = store[channelKey] or {}
  channels[name] = {channel:destination, name:name}
  store[channelKey] = channels
  reply "Ok. Subscribed to #{bold name}"
  checkChannels()

unsubscribe = (source, destination, name) =>
  channelKey = key..':'..destination
  channels = store[channelKey] or {}
  unless channels[name]
    reply "Wasn't subscribed. But, sure."
  else
    channels[name] = nil
    store[channelKey] = channels
    reply "Ok. Stopped caring about #{bold name}"

list = (source, destination) =>
  channelKey = key..':'..destination
  channels = store[channelKey] or {}
  out = {}
  for name, game in pairs(channels)
      out[#out+1] = name
  say "Subscribed to: #{table.concat(out, ', ')}"

-- Start the subscribe poller
ivar2\Timer('youtube', 300, 300, checkChannels)

PRIVMSG:
  '^%pyoutube latest (.*)': getLatest
  '^%pyoutube subscribe (.*)': subscribe
  '^%pyoutube unsubscribe (.*)': unsubscribe
  '^%pyoutube list': list


Fixed 'require' method in Heuristic sandbox.

- Also exposed base Filter class to sandboxed Heuristics.

-- This Source Code Form is subject to the terms of the Mozilla Public
-- License, v. 2.0. If a copy of the MPL was not distributed with this
-- file, You can obtain one at http://mozilla.org/MPL/2.0/.
--
-- Copyright 2016 [bk]door.maus

moonscript = require "moonscript"
Log = require "glurg.common.Log"
Promise = require "glurg.common.Promise"
Trace = require "glurg.trace.Trace"

clone_global_table = ->
	g = {
		glurg: {
			common: {
				Log: require "glurg.common.Log"
				Promise: require "glurg.common.Promise"
				Retrace: require "glurg.common.Retrace"
			},
			filters: {
				BufferFilter: require "glurg.filters.BufferFilter"
				Filter: require "glurg.filters.Filter"
				ProgramFilter: require "glurg.filters.ProgramFilter"
				VertexArrayFilter: require "glurg.filters.VertexArrayFilter"
			},
			heuristics: {
				GL3StaticModelHeuristic: require "glurg.heuristics.GL3StaticModelHeuristic"
			},
			resources: {
				Mesh: require "glurg.resources.Mesh"
				MeshBlob: require "glurg.resources.MeshBlob"
				MeshBlobBuilder: require "glurg.resources.MeshBlobBuilder"
				Model: require "glurg.resources.Model"
				Texture: require "glurg.resources.Texture"
				TextureBlob: require "glurg.resources.TextureBlob"
				TextureBlobBuilder: require "glurg.resources.TextureBlobBuilder"
			},
			trace: {
				Call: require "glurg.trace.Call"
				Event: require "glurg.trace.Event"
				Trace: require "glurg.trace.Trace"
				Value: require "glurg.trace.Value"
			}
		},

		:error,
		:getfenv,
		:getmetatable,
		:ipairs,
		:next,
		:pairs,
		:pcall,
		:print,
		:rawequal,
		:rawget,
		:rawset,
		:select,
		:setfenv,
		:setmetatable,
		:tonumber,
		:tostring,
		:type,
		:unpack,
		:xpcall,

		:coroutine,
		:io,
		:math,
		:os,
		:string,
		:table
	}
	g.require = (path) ->
		s = moonscript.loadfile(path .. ".moon")
		g.setfenv(s, g)
		return s()
	g._G = g

	return g

SwapBuffers = {
	"glXSwapBuffers": true
}

class Heuristic
	new: (filename) =>
		success, result = pcall(require, "glurg.heuristics." .. filename)

		if success then
			@heuristic = result
		else
			func, m = moonscript.loadfile(filename)
			if func == nil then
				error m, 0

			@heuristic = setfenv(func, clone_global_table!)!

		@data = {}
		@lower_call_range = nil
		@upper_call_range = nil
		@lower_frame_range = nil
		@upper_frame_range = nil

	set_data: (key, value) =>
		@data[key] = value

	run: (filename) =>
		heuristic = self.heuristic(@data)
		filters = { heuristic\get_filters! }

		trace = Trace(filename)
		current_frame = 1
		while not trace.is_end_of_file
			event = trace\next_event!
			if event.type == 'leave'
				call_name = event.call.signature.name
				can_delete_call = true

				for i = 1, #filters
					func = filters[i]["gl"]
					if filters[i][call_name]
						func = filters[i][call_name]

					if func != nil then
						r = func(filters[i], trace, event.call)
						if r then
							can_delete_call = false

				is_lower_call = not @lower_call_range or event.call_index >= @lower_call_range
				is_upper_call = not @upper_call_range or @upper_call_range >= event.call_index
				is_lower_frame = not @lower_frame_range or current_frame >= @lower_frame_range
				is_upper_frame = not @upper_frame_range or @upper_frame_range >= current_frame

				if is_lower_call and is_lower_frame and is_upper_call and is_upper_frame then
					func = heuristic["gl"]
					if heuristic[call_name]
						func = heuristic[call_name]

					if func != nil then
						r = func(heuristic, trace, event.call)
						if r then
							can_delete_call = false

				if SwapBuffers[call_name]
					Log\info('low', "finished frame #{current_frame}")
					current_frame += 1

				if can_delete_call then
					trace\delete_call(event.call_index)

		if heuristic.dispose
			heuristic\dispose(trace)

return Heuristic

-- This Source Code Form is subject to the terms of the Mozilla Public
-- License, v. 2.0. If a copy of the MPL was not distributed with this
-- file, You can obtain one at http://mozilla.org/MPL/2.0/.
--
-- Copyright 2016 [bk]door.maus

moonscript = require "moonscript"
Log = require "glurg.common.Log"
Promise = require "glurg.common.Promise"
Trace = require "glurg.trace.Trace"

clone_global_table = ->
	g = {
		glurg: {
			common: {
				Log: require "glurg.common.Log"
				Promise: require "glurg.common.Promise"
				Retrace: require "glurg.common.Retrace"
			},
			filters: {
				BufferFilter: require "glurg.filters.BufferFilter"
				ProgramFilter: require "glurg.filters.ProgramFilter"
				VertexArrayFilter: require "glurg.filters.VertexArrayFilter"
			},
			heuristics: {
				GL3StaticModelHeuristic: require "glurg.heuristics.GL3StaticModelHeuristic"
			},
			resources: {
				Mesh: require "glurg.resources.Mesh"
				MeshBlob: require "glurg.resources.MeshBlob"
				MeshBlobBuilder: require "glurg.resources.MeshBlobBuilder"
				Model: require "glurg.resources.Model"
				Texture: require "glurg.resources.Texture"
				TextureBlob: require "glurg.resources.TextureBlob"
				TextureBlobBuilder: require "glurg.resources.TextureBlobBuilder"
			},
			trace: {
				Call: require "glurg.trace.Call"
				Event: require "glurg.trace.Event"
				Trace: require "glurg.trace.Trace"
				Value: require "glurg.trace.Value"
			}
		},

		:error,
		:getfenv,
		:getmetatable,
		:ipairs,
		:next,
		:pairs,
		:pcall,
		:print,
		:rawequal,
		:rawget,
		:rawset,
		:select,
		:setfenv,
		:setmetatable,
		:tonumber,
		:tostring,
		:type,
		:unpack,
		:xpcall,

		:coroutine,
		:io,
		:math,
		:os,
		:string,
		:table
	}
	g.require = (path) ->
		return _dofile(path .. ".moon")
	g._G = g

	return g

SwapBuffers = {
	"glXSwapBuffers": true
}

class Heuristic
	new: (filename) =>
		success, result = pcall(require, "glurg.heuristics." .. filename)

		if success then
			@heuristic = result
		else
			func, m = moonscript.loadfile(filename)
			if func == nil then
				error m, 0

			@heuristic = setfenv(func, clone_global_table!)!

		@data = {}
		@lower_call_range = nil
		@upper_call_range = nil
		@lower_frame_range = nil
		@upper_frame_range = nil

	set_data: (key, value) =>
		@data[key] = value

	run: (filename) =>
		heuristic = self.heuristic(@data)
		filters = { heuristic\get_filters! }

		trace = Trace(filename)
		current_frame = 1
		while not trace.is_end_of_file
			event = trace\next_event!
			if event.type == 'leave'
				call_name = event.call.signature.name
				can_delete_call = true

				for i = 1, #filters
					func = filters[i]["gl"]
					if filters[i][call_name]
						func = filters[i][call_name]

					if func != nil then
						r = func(filters[i], trace, event.call)
						if r then
							can_delete_call = false

				is_lower_call = not @lower_call_range or event.call_index >= @lower_call_range
				is_upper_call = not @upper_call_range or @upper_call_range >= event.call_index
				is_lower_frame = not @lower_frame_range or current_frame >= @lower_frame_range
				is_upper_frame = not @upper_frame_range or @upper_frame_range >= current_frame

				if is_lower_call and is_lower_frame and is_upper_call and is_upper_frame then
					func = heuristic["gl"]
					if heuristic[call_name]
						func = heuristic[call_name]

					if func != nil then
						r = func(heuristic, trace, event.call)
						if r then
							can_delete_call = false

				if SwapBuffers[call_name]
					Log\info('low', "finished frame #{current_frame}")
					current_frame += 1

				if can_delete_call then
					trace\delete_call(event.call_index)

		if heuristic.dispose
			heuristic\dispose(trace)

return Heuristic


cleanup

cleanup

html = require "lapis.html"
config = (require "lapis.config").get!
appname=config.appname or "l0g1n.de"

class MainLayout extends html.Widget
  content: =>
   html_5 ->
      head ->
        meta charset:"utf-8"
        meta name:"viewport", content:"width=device-width, initial-scale=1"
        link rel:"stylesheet", href:"/static/css/pure-min.css"
        link rel:"stylesheet", href:"/static/css/main_layout.less.css"
        script src:"/static/js/jquery.min.js"
        script src:"/static/js/dropdownmenu.js"
        title (
          (@title and @title.." - "..appname) or
          (@has_content_for("title") and @content_for("title").." - "..appname) or
          appname
        )
      body class:"route-"..@route_name..(type(@body_classes)=="string" and " "..@body_classes or ""),->
        render "views.widgets.header"
        div class:"content",->
          @content_for "inner"

html = require "lapis.html"
config = (require "lapis.config").get!
appname=config.appname or "l0g1n.de"

class MainLayout extends html.Widget
  --@include "views.mixin.header"
  --@include "views.mixin.favicon"
  --@include "lazuli.modules.user_management.views.mixin_menu"
  content: =>
   html_5 ->
      head ->
        meta charset:"utf-8"
        meta name:"viewport", content:"width=device-width, initial-scale=1"
        link rel:"stylesheet", href:"/static/css/pure-min.css"
        link rel:"stylesheet", href:"/static/css/main_layout.less.css"
        script src:"/static/js/jquery.min.js"
        script src:"/static/js/dropdownmenu.js"
        title (
          (@title and @title.." - "..appname) or
          (@has_content_for("title") and @content_for("title").." - "..appname) or
          appname
        )
        --@mixFavicon!
      body class:"route-"..@route_name..(type(@body_classes)=="string" and " "..@body_classes or ""),->
        --@mixin_header!
        render "views.widgets.header"
        div class:"content",->
          @content_for "inner"