diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/Makefile lua-orbit-2.2.1+dfsg/Makefile --- lua-orbit-2.2.0+gita6fb46e+dfsg/Makefile 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/Makefile 2014-02-02 09:25:01.000000000 +0000 @@ -11,12 +11,12 @@ mkdir -p $(LUA_DIR) cp src/orbit.lua $(LUA_DIR) mkdir -p $(LUA_DIR)/orbit - cp src/model.lua $(LUA_DIR)/orbit - cp src/cache.lua $(LUA_DIR)/orbit - cp src/pages.lua $(LUA_DIR)/orbit - cp src/ophandler.lua $(LUA_DIR)/orbit + cp src/orbit/model.lua $(LUA_DIR)/orbit + cp src/orbit/cache.lua $(LUA_DIR)/orbit + cp src/orbit/pages.lua $(LUA_DIR)/orbit + cp src/orbit/ophandler.lua $(LUA_DIR)/orbit mkdir -p $(BIN_DIR) - cp src/orbit $(BIN_DIR) + cp src/launchers/orbit $(BIN_DIR) if [ -f ./wsapi/Makefile ]; then \ cd wsapi && make install; \ fi diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/README lua-orbit-2.2.1+dfsg/README --- lua-orbit-2.2.0+gita6fb46e+dfsg/README 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/README 2014-02-02 09:25:01.000000000 +0000 @@ -1,4 +1,4 @@ -Orbit 2.2.0 +Orbit 2.2.1 http://keplerproject.github.com/orbit Orbit is an MVC web framework for Lua. The design is inspired by lightweight @@ -11,6 +11,14 @@ History +* Version 2.2.1 (12/Jan/2014) + bugfix release for Lua 5.1 + NOT 5.2 compliant + documentation corrections + support for Wsapi 1.6 and other dependency modules that no longer use "module" + additional orbit model datatypes: real, float, timestamp, numeric + MIME type application/json included + * Version 2.2.0 (31/Mar/2010) Reparse response to resume the dispatcher better parser for orbit.model conditions, fixes parsing bugs @@ -63,7 +71,7 @@ Go to the path where LuaRocks put Orbit to see the sample apps and this documentation. LuaRocks will automatically fetch and install any dependencies you don't already have. -To run the supplied example, go to the samples directory +To run the supplied example, go to the samples/hello directory of this distribution and do: orbit hello.lua diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/debian/changelog lua-orbit-2.2.1+dfsg/debian/changelog --- lua-orbit-2.2.0+gita6fb46e+dfsg/debian/changelog 2013-08-13 08:18:38.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/debian/changelog 2014-02-02 09:39:01.000000000 +0000 @@ -1,3 +1,10 @@ +lua-orbit (2.2.1+dfsg-1) unstable; urgency=medium + + * New upstream release + * Clarify license name in debian/copyright + + -- Enrico Tassi Sun, 02 Feb 2014 10:26:44 +0100 + lua-orbit (2.2.0+gita6fb46e+dfsg-1) unstable; urgency=low * Clean up orig tarballs to comply to DFSG diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/debian/copyright lua-orbit-2.2.1+dfsg/debian/copyright --- lua-orbit-2.2.0+gita6fb46e+dfsg/debian/copyright 2013-08-13 08:18:38.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/debian/copyright 2014-02-02 09:39:01.000000000 +0000 @@ -5,18 +5,26 @@ Files: * Copyright: 2007-2008 The Kepler Project. -License: MIT +Comment: In the Lua community this license is better known as "MIT". + Unfortunately other variants of this license are also known as "MIT". + To obtain a machine intepretable copyright file Debian prefers to name this + version of the MIT license using the non ambiguous term "Expat". +License: Expat Files: samples/toycms/markdown.lua Copyright: 2007 Niklas Frykholm -License: MIT +License: Expat + +Files: doc/us/markdown.lua +Copyright: 2007 Niklas Frykholm +License: Expat Files: samples/todo/jquery-1.2.3*.js Copyright: 2008 John Resig (jquery.com) -License: GPL-2 or MIT +License: GPL-2 or Expat A copy of the GPL license can be found in /usr/share/common-licenses. -License: MIT +License: Expat Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/doc/br/blog_config.lua lua-orbit-2.2.1+dfsg/doc/br/blog_config.lua --- lua-orbit-2.2.0+gita6fb46e+dfsg/doc/br/blog_config.lua 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/doc/br/blog_config.lua 1970-01-01 00:00:00.000000000 +0000 @@ -1,160 +0,0 @@ - --- Puts all the definitions below in blog's namespace -module("blog", package.seeall) - -blog_title = "Blog" - -cache_path = "page_cache" - -copyright_notice = "Copyright 2007 Foobar" - -about_blurb = [[This is an example of a blog built using Orbit. You -can browse posts and add comments, but to add new posts you have -to go directly to the database. This will be fixed in the future.]] - -blogroll = { - { "http://slashdot.org", "Slashdot"}, - { "http://news.google.com", "Google News" }, - { "http://www.wikipedia.org", "Wikipedia" }, -} - --- Uncomment this to send static files through X-Sendfile --- use_xsendfile = true - -database = { --- driver = "mysql", --- conn_data = { "blog", "root", "password" } - driver = "sqlite3", - conn_data = { blog.real_path .. "/blog.db" } -} - -recent_count = 7 - -strings = {} - -strings.pt = { - home_page_name = "Página Inicial", - about_title = "Sobre o Blog", - last_posts = "Últimos Posts", - blogroll_title = "Links", - archive_title = "Arquivo", - anonymous_author = "Anônimo", - no_posts = "No há posts publicados.", - published_at = "Publicado às", - comments = "Comentários", - written_by = "Escrito por", - on_date = "em", - new_comment = "Novo comentário", - no_comment = "Você esqueceu o comentário!", - form_name = "Nome:", - form_email = "Email:", - form_url = "Site:", - italics = "itálico", - bold = "negrito", - link = "link", - send = "Enviar" -} - -strings.en = { - home_page_name = "Home Page", - about_title = "About this Blog", - last_posts = "Recent Posts", - blogroll_title = "Links", - archive_title = "Archives", - anonymous_author = "Anonymous", - no_posts = "No published posts.", - published_at = "Published at", - comments = "Comments", - written_by = "Written by", - on_date = "at", - new_comment = "New comment", - no_comment = "You forgot the comment!", - form_name = "Name:", - form_email = "Email:", - form_url = "Site:", - italics = "italics", - bold = "bold", - link = "link", - send = "Send" -} - -language = "en" - -strings = strings[language] - -months = {} - -months.pt = { "Janeiro", "Fevereiro", "Março", "Abril", - "Maio", "Junho", "Julho", "Agosto", "Setembro", "Outubro", - "Novembro", "Dezembro" } - -months.en = { "January", "February", "March", "April", - "May", "June", "July", "August", "September", "October", - "November", "December" } - -weekdays = {} - -weekdays.pt = { "Domingo", "Segunda", "Terça", "Quarta", - "Quinta", "Sexta", "Sábado" } - -weekdays.en = { "Sunday", "Monday", "Tuesday", "Wednesday", - "Thursday", "Friday", "Saturday" } - --- Utility functions - -time = {} -date = {} -month = {} - -local datetime_mt = { __call = function (tab, date) return tab[language](date) end } - -setmetatable(time, datetime_mt) -setmetatable(date, datetime_mt) -setmetatable(month, datetime_mt) - -function time.pt(date) - local time = os.date("%H:%M", date) - date = os.date("*t", date) - return date.day .. " de " - .. months.pt[date.month] .. " de " .. date.year .. " às " .. time -end - -function date.pt(date) - date = os.date("*t", date) - return weekdays.pt[date.wday] .. ", " .. date.day .. " de " - .. months.pt[date.month] .. " de " .. date.year -end - -function month.pt(month) - return months.pt[month.month] .. " de " .. month.year -end - -local function ordinalize(number) - if number == 1 then - return "1st" - elseif number == 2 then - return "2nd" - elseif number == 3 then - return "3rd" - else - return tostring(number) .. "th" - end -end - -function time.en(date) - local time = os.date("%H:%M", date) - date = os.date("*t", date) - return months.en[date.month] .. " " .. ordinalize(date.day) .. " " .. - date.year .. " at " .. time -end - -function date.en(date) - date = os.date("*t", date) - return weekdays.en[date.wday] .. ", " .. months.en[date.month] .. " " .. - ordinalize(date.day) .. " " .. date.year -end - -function month.en(month) - return months.en[month.month] .. " " .. month.year -end - diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/doc/br/example.html lua-orbit-2.2.1+dfsg/doc/br/example.html --- lua-orbit-2.2.0+gita6fb46e+dfsg/doc/br/example.html 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/doc/br/example.html 1970-01-01 00:00:00.000000000 +0000 @@ -1,701 +0,0 @@ - - - - - Orbit - - - - - - -
- -
- - -
Orbit
-
MVC desenvolvimento Web em Lua
-
- -
- - - -
- - -

Tutorial Orbit

- -

Este tutorial mostra como criar uma aplicação blog simples, apoiada por uma base de dados. Ela é muito simples porque não inclui nenhuma página de administração ("admin"); você tem que adicionar posts diretamente na base de dados (embora você possa postar através de um console Lua, e este tutorial mostrará como), mas tem uma interface para comentários nos posts.

- -

O tutorial parte do princípio que você já tem o Orbit instalado (preferencialmente como parte do Kepler, ou pelo LuaRocks, e já tem um servidor web que aceita configuração WSAPI (o servidor web Xavante que vem com o Kepler é uma boa escolha).

- -

Os códigos fontes para este blog estão nas pastas ´samples´ da distribuição do Orbit. Se você instalou o Orbit pelo Kepler ou LuaRocks, veja dentro da pasta ´rocks´ da sua instalação.

- -

Inicialização

- -

Você deve criar um arquivo ´blog.lua´, que será o arquivo principal de códigos fontes da nossa aplicação. A primeira coisa que você deve por no arquivo é o código para carregar o Orbit e outras bibliotecas que você usará no seu aplicativo.

- - -
 
-require "orbit" 
-require "orbit.cache" 
-require "markdown" 
-
- - -

Neste exemplo usaremos a página de  cache do Orbit, e o parser Markdown para marcar os posts.

- -

Todas as aplicações Orbit são módulos Lua, portanto incluiremos esta linha:

- - -
 
-module("blog", package.seeall, orbit.app) 
-
- - -

Isso configura o módulo ´blog´ e o inicializa como uma aplicação Orbit.

- -

´orbit.app´ coloca muitas coisas no namespace do módulo ´blog´. Os mais importantes são os metódos ´dispatchget´, ´dispatchpost´ e ´model´ que permitem que você defina a funcionalidade principal da sua aplicação. Eles também definem a variável ´mapper´ que o Orbit usa para criar os modelos (Orbit inicializa essa variável para o seu mapeador objeto-relacional padrão). Por último, eles também definem os controles padrões para os códigos de erros 404 e 500 HTTP como as variáveis ´notfound´ e ´servererror´, respectivamente. Redefina essas variáveis se quiser páginas customizadas para a sua aplicação.

- -

Vamos carregar um script de configuração para o blog (um modelo comum em aplicações). Você pode pegar este script aqui.

- - -
 
-require "blog_config" 
-
- - -

As próximas linhas carregam um driver de base de dados LuaSQL (definido na configuração), e configura o mapeador objeto relacional do Orbit.

- - -
 
-require("luasql." .. database.driver) 
-local env = luasql[database.driver]() 
-mapper.conn = env:connect(unpack(database.conn_data)) 
-mapper.driver = database.driver 
-
- - -

O mapeador do Orbit precisa usar uma conexão de base de dados, e de qual driver você estiver usando (no momento apenas o "sqlite3" e "mysql" são aceitos).

- -

Você precisa iniciar o mapeador antes de criar os modelos de sua aplicação porque o mapeador do Orbit consulta a base de dados durante a criação de modelos para pegar o esquema. Falando em esquema, agora é uma boa hora para criar a base de dados do seu blog. Parto do princípio que você está usando o SQLite3. Crie uma base de dados ´blog.db´ com o script SQL abaixo:

- - -
 
-CREATE TABLE blog_post 
-("id" INTEGER PRIMARY KEY NOT NULL, 
-"title" VARCHAR(255) DEFAULT NULL, 
-"body" TEXT DEFAULT NULL, 
-"n_comments" INTEGER DEFAULT NULL, 
-"published_at" DATETIME DEFAULT NULL); 
-
-CREATE TABLE blog_comment 
-("id" INTEGER PRIMARY KEY NOT NULL, 
-"post_id" INTEGER DEFAULT NULL, 
-"author" VARCHAR(255) DEFAULT NULL, 
-"email" VARCHAR(255) DEFAULT NULL, 
-"url" VARCHAR(255) DEFAULT NULL, 
-"body" TEXT DEFAULT NULL, 
-"created_at" DATETIME DEFAULT NULL); 
-
-CREATE TABLE blog_page 
-("id" INTEGER PRIMARY KEY NOT NULL, 
-"title" VARCHAR(30) DEFAULT NULL, 
-"body" TEXT DEFAULT NULL); 
-
- - -

O mapeador do Orbit usa o campo ´id´ para identificar objetos na base de dados, portanto você precisará de um para cada um dos tipos de objetos que estiver mapeando.

- -

Por último, vamos iniciar o cache de páginas do Orbit antes de criar nossos modelos:

- - -
 
-local cache = orbit.cache.new(blog, cache_path) 
-
- - -

O cache de páginas acelera o acesso a qualquer página que você cacheie, mas você precisará ser cuidadoso e limpar o cache para uma página quando qualquer conteúdo nela mudar. Veremos como cachear e invalidar páginas na seção de controle deste tutorial.

- -

Criando Modelos

- -

Nossa aplicação de blog tem três tipos de objetos: posts, comentários e páginas "estáticas" (como a página de "Sobre" do blog, por exemplo). Não é coincidência que também temos três tipos de tabelas na base de dados, cada tabela mapeia um tipo de objeto que a nossa aplicação reconhece, e para cada tipo criaremos um modelo. Primeiro criaremos um objeto modelo para posts:

- - -
 
-posts = blog:model "post" 
-
- - -

O parâmetro para o método ´model´ é o nome de uma tabela na base de dados. O objeto ´posts´ que esse método cria representa a coleção de posts, e ao mesmo tempo é um protótipo para todos os posts (veremos as implicação disso em breve). O mapeador do Orbit cria um objeto funcional por conta própria: você pode fazer ´post:find(3)´, por exemplo, e pegar o post com ´id´ 3, ou ´post:findall("ncomments < ?", { 3, order = "published_at desc"})´ e ter uma lista de todos os posts com menos de três comentários, do mais recente ao mais antigo.

- -

Você pode usar o método ´find´ pré-definido para todas as buscas na base de dados, mas ajuda simplificar buscas comuns nos seus métodos. Você pode fazer isso adicionando métodos no objeto ´posts´:

- - -
 
-function posts:find_recent() 
-return self:find_all("published_at is not null", 
-{ order = "published_at desc", 
-count = recent_count }) 
-end 
-
- - -

As linhas acima adicionam um método ´find_recent´ no objeto ´posts´, retornando uma lista dos posts publicados mais recentementes (o número está no script de configuração), do mais recente ao mais antigo. A aplicação irá usar este método para gerar a lista de posts na home page, assim como a seção "Posts recentes" na lateral do blog.

- -

Outra característica do nosso blog será a página de arquivo que mostra todos os posts de um certo mês e ano. Definiremos um método para isto também:

- - -
 
-function posts:find_by_month_and_year(month, year) 
-local s = os.time({ year = year, month = month, day = 1 }) 
-local e = os.time({ year = year + math.floor(month / 12), 
-month = (month % 12) + 1, 
-day = 1 }) 
-return self:find_all("published_at >= ? and published_at < ?", 
-{ s, e, order = "published_at desc" }) 
-end 
-
- - -

Este é o método mais complicado, já que temos que converter de um mês e ano simples para data de começo e fim no formato Lua padrão. Por último, também definiremos um método para retornar todos os meses (e anos) que tem posts, para mais tarde gerar os links para a seção "Arquivo" no sidebar:

- - -
 
-function posts:find_months() 
-local months = {} 
-local previous_month = {} 
-local posts = self:find_all("published_at is not null", 
-{ order = "published_at desc" }) 
-for _, post in ipairs(posts) do 
-local date = os.date("*t", post.published_at) 
-if previous_month.month ~= date.month or 
-previous_month.year ~= date.year then 
-previous_month = { month = date.month, year = date.year, 
-date_str = os.date("%Y/%m", post.published_at) } 
-months[#months + 1] = previous_month 
-end 
-end 
-return months 
-end 
-
- - -

Este método pega todos os posts na base de dados, ordenados por data, e iterates over them armazenando cada par de mês e ano numa lista.

- -

We can also define methods for individual post objects by defining methods -in the posts object, the only difference is how they are used (you use find_recent -by doing posts:find_recent(), but you will use find_comments by doing p:find_comments(), -where p is a particular post object. We will define a method to retrieve all comments of -a post:

- -

Também podemos definir métodos para objetos de posts individuais definindo métodos no objeto ´posts´, a única diferença é como eles são usados (você usa ´findrecent´ criando ´posts:findrecent()´, mas você usará ´findcomments´ criando ´p:findcomments()´, onde ´p´ é um objeto post específico. Definiremos um método para recuperar todos os comentários de um post:

- - -
 
-function posts:find_comments() 
-return comments:find_all_by_post_id{ self.id } 
-end 
-
- - -

Este método usa um método pré-definido do objeto ´comments´ (que criaremos em breve) que pega todos os comentários com o campo ´post_id´ iguais ao id do post atual (´self.id´). Este método cria uma relação entre os posts e os comentários; uma versão futura do mapeador Orbit permitirá que você defina isso declaradamente.

- -

Criar o objeto ´comments´ é simples:

- - -
 
-comments = blog:model "comment" 
-
- - -

Vamos adicionar um método de conveniência para comentários que constróem o link de comentário a partir de seus dados:

- - -
 
-function comments:make_link() 
-local author = self.author or strings.anonymous_author 
-if self.url and self.url ~= "" then 
-return "" .. author .. "" 
-elseif self.email and self.email ~= "" then 
-return "" .. author .. "" 
-else 
-return author 
-end 
-end 
-
- - -

O objeto ´pages´ é mais simples ainda, a funcionalidade padrão fornecida pelo mapeador Orbit é suficiente, então criaremos com o ´model´:

- - -
 
-pages = blog:model "pages" 
-
- - -

Isso conclui a parte "model" da nossa aplicação. Agora podemos seguir para definir a flow página da aplicação, definindo controllers e mapeando-os para URLs.

- -

Definindo controladores

- -

Controladores são a interface entre a web e sua aplicação. Com o Orbit você pode -mapear a parte path das URLs de sua aplicação (pore exemplo, em http://myserver.com/myapp.ws/foo/bar -o path é /foo/bar) em controladores. Em termos Lua, um controlador Orbit é uma função que recebe -um objeto request/response (chamado web) e parâmetros obtidos do path, e retorna um texto -que é enviado para o cliente (geralmente HTML, mas pode ser XML ou mesmo uma imagem).

- -

Você mapeia paths em controladores com os métodos dispatch_get e dispatch_post -para requisições GET e POST respectivamente. O primeiro parâmetro destes métodos é o -controlador, uma função Lua, e todos os outros parâmetros são os padrões de mapeamento, -escritos na sintaxe de padrões de strings de Lua, de forma que um controlador pode responder -a mapeamentos diversos.

- -

Abaixo esta o controlador para a pagina principal do blog:

- - -
 
-function index(web) 
-local ps = posts:find_recent() 
-local ms = posts:find_months() 
-local pgs = pgs or pages:find_all() 
-return render_index(web, { posts = ps, months = ms, 
-recent = ps, pages = pgs }) 
-end 
-
-blog:dispatch_get(cache(index), "/", "/index") 
-
- - -

A última linha estabelece o mapeamento entre a função index e root da aplicação. -O chamado do cache define o caching para esse controlador, -usando o cache que criamos anteriormente (este é outro idioma comum do Lua, -funciona como"decorators").

- -

O controlador indexmostra todos os posts recentes, e é bem direto. Ele somente chama os dados modelo solicitados apartir do banco de dados, depois chama uma função auxiliar (chamada view na tecnologia MVC) para  renderizar o código HTML atual.

- -

Outro importante controlador é o que mostra posts únicos:

- - -
 
-function view_post(web, post_id, comment_missing) 
-local post = posts:find(tonumber(post_id)) 
-if post then 
-local recent = posts:find_recent() 
-local pgs = pages:find_all() 
-post.comments = post:find_comments() 
-local months = posts:find_months() 
-return render_post(web, { post = post, months = months, 
-recent = recent, pages = pgs, 
-comment_missing = comment_missing }) 
-else 
-return not_found(web) 
-end 
-end 
-
-blog:dispatch_get(cache(view_post), "/post/(%d+)") 
-
- - -

Aqui nós mapeamos todos os paths como /post/53 para o controlador view_post. O pattern captura os números, e é passado por todo controlador pelo Orbit. Para /post/53, o controlador recebe a string ''53'' como post_id e usa isto para chamar o post correspondente. Novamente, a renderização do HTML esta factored out para outra função, e este controlador esta cached.

- -

Se nenhum post com este id for encontrado, o controlador default de páginas perdidas será chamado blog.not_found (orbit.app coloque isto no  namespace do blog)

- -

Arquivos e paginas tem estruturas similares:

- - - -
 
-function view_archive(web, year, month) 
-local ps = posts:find_by_month_and_year(tonumber(month), 
-tonumber(year)) 
-local months = posts:find_months() 
-local recent = posts:find_recent() 
-local pgs = pages:find_all() 
-return render_index(web, { posts = ps, months = months, 
-recent = recent, pages = pgs }) 
-end 
-
-blog:dispatch_get(cache(view_archive), "/archive/(%d%d%d%d)/(%d%d)") 
-
-function view_page(web, page_id) 
-local page = pages:find(tonumber(page_id)) 
-if page then 
-local recent = posts:find_recent() 
-local months = posts:find_months() 
-local pgs = pages:find_all() 
-return render_page(web, { page = page, months = months, 
-recent = recent, pages = pgs }) 
-else 
-not_found(web) 
-end 
-end 
-
-blog:dispatch_get(cache(view_page), "/page/(%d+)") 
-
- - -

Os arquivos utilizam o mesmo layout que o index, logo ele reutiliza seu gerador de HTML. Os arquivos também extraem dois parametros do path, o mês e o ano, logo os paths são como /archive/2008/05.

- -

Finalmente você pode também pode definir arquivos estáticos com o método de conveniência dispatch_static.

- - -
 
-blog:dispatch_static("/head%.jpg", "/style%.css") 
-
- - -

Esses também são patterns, logo os pontos são escapados. Você pode definir em sua aplicação, uma pasta como estática com blog:dispatch_static("/templates/.+"). O Orbit sempre procura pelos arquivos nas pastas das aplicações. Claro que você pode deixar sua aplicação comportar somente conteúdos dinâmicos e deixar seu servidor web servir conteúdo estátic; dispatch_static é só uma conveniência para ter aplicações "zero-configuration"

- -

Tem um controlador para adicionar comentários. Este irá responder ao POST em vez de receber:

- - -
 
-function add_comment(web, post_id) 
-local input = web.input 
-if string.find(input.comment, "^%s*$") then 
-return view_post(web, post_id, true) 
-else 
-local comment = comments:new() 
-comment.post_id = tonumber(post_id) 
-comment.body = markdown(input.comment) 
-if not string.find(input.author, "^%s*$") then 
-comment.author = input.author 
-end 
-if not string.find(input.email, "^%s*$") then 
-comment.email = input.email 
-end 
-if not string.find(input.url, "^%s*$") then 
-comment.url = input.url 
-end 
-comment:save() 
-local post = posts:find(tonumber(post_id)) 
-post.n_comments = (post.n_comments or 0) + 1 
-post:save() 
-cache:invalidate("/") 
-cache:invalidate("/post/" .. post_id) 
-cache:invalidate("/archive/" .. os.date("%Y/%m", post.published_at)) 
-return web:redirect(web:link("/post/" .. post_id)) 
-end 
-end 
-
-blog:dispatch_post(add_comment, "/post/(%d+)/addcomment") 
-
- - -

O controlador add_comment primeiro valida o input, delegando ao view_post se o campo de comentário estiver vazio (o qual irá mostrar uma menssagem de erro na página). Você acessa o parâmetro POST pela tabela web.input, que é convenientemente  aliased para um input local variável. 

- -

O controlador cria um novo objeto comment, preenche com dados e depois salva no banco de dados. Ele também atualiza o objeto post para aumentar o número de comment o post tem por um, alem de salvar. Depois continua para invalidar (em cache) todas as páginas que talvez mostre esta informação: o index, a página de postagem e os arquivos para este post em particular. Finalmente, ele redireciona para a página de postagem, que irá mostrar o novo comentário. Este é um idioma comum na programação web chamada POST-REDIRECT-GET, onde todo POST é seguido por um redirecionamente para um GET. Isso evita a dupla postagem no caso do usuário carregar a página novamente.

- -

A única coisa que resta agora é a geração de HTML. Está é um tópico da próxima sessão.

- -

Visualizações: Gerando HTML

- -

Visualizações é o último componente do trio MVC. Para o Orbit, visualizações são funções simples que geram conteúdo (geralmente HTML), e são estritamente opcionais, o que significa que você pode devolver conteúdo diretamente do seu controle. Mas ainda é bom ter prática em programação para separar controles e visualizações.

- -

Como você gera conteúdo é escolha sua: concatene correntes Lua, use ´table.concat´, use um template de biblioteca de terceiros... Orbit fornece geração de HTML/XML programático através de órbit.htmlify´, mas você está livre para usar qualquer método que preferir. Neste tutorial manteremos a geração programática, embora, assim como outros métodos (strings retas, Cosmo, etc.) são inteiramente documentadas em outro lugar.

- -

Quando você htmlify uma função, o Orbit muda o ambiente da função permitindo que você gere HTML chamando as tags de funções. É melhor mostrar como funciona do que explicar, então aqui vai a visualização básica da aplicação do blog, ´layout´:

- - -
 
-function layout(web, args, inner_html) 
-return html{ 
-head{ 
-title(blog_title), 
-meta{ ["http-equiv"] = "Content-Type", 
-content = "text/html; charset=utf-8" }, 
-link{ rel = 'stylesheet', type = 'text/css', 
-href = web:static_link('/style.css'), media = 'screen' } 
-}, 
-body{ 
-div{ id = "container", 
-div{ id = "header", title = "sitename" }, 
-div{ id = "mainnav", 
-_menu(web, args) 
-}, 
-div{ id = "menu", 
-_sidebar(web, args) 
-}, 
-div{ id = "contents", inner_html }, 
-div{ id = "footer", copyright_notice } 
-} 
-} 
-} 
-end 
-
- - -

Esta visualização é um decorador para outras visualizações, e gera o boilerplate para cada página do blog (cabeçalho, rodapé, sidebar). Você pode ver as funções de gerador HTML por todo o código, como ´title´, ´html´, ´head´, ´div´. Cada um tem ou uma string ou uma tabela, e gera o HTML correspondente. Se você dispensar uma tabela, a parte de banco de dados é concatenada e usada como conteúdo, enquanto a parte hash é usada como atributos HTML para aquela tag. Uma tag sem conteúdo gera um tag self-closing (´meta´ e ´link´ no código acima).

- -

Digno de nota no código acima são as chamadas ´web:staticlink´ e as funções ´menu´ e ´sidebar´.O método ´staticlink´ gera um link para um recurso estático da aplicação, tirando o SCRIPT_NAME da URL (por exemplo, se a URL é http://myserver.com/myblog/blog.ws/index irá voltar como /myblog/style.css como o link).

- -

As funções ´menu´ e ´sidebar´ são apenas visualizações de ajuda para gerar a barra de menu e sidebar do blog:

- - -
 
-function _menu(web, args) 
-local res = { li(a{ href= web:link("/"), strings.home_page_name }) } 
-for _, page in pairs(args.pages) do 
-res[#res + 1] = li(a{ href = web:link("/page/" .. page.id), page.title }) 
-end 
-return ul(res) 
-end 
-
-function _sidebar(web, args) 
-return { 
-h3(strings.about_title), 
-ul(li(about_blurb)), 
-h3(strings.last_posts), 
-_recent(web, args), 
-h3(strings.blogroll_title), 
-_blogroll(web, blogroll), 
-h3(strings.archive_title), 
-_archives(web, args) 
-} 
-end 
-
- - -

Aqui você vê uma mistura de idiomas básicos do Lua (preenchendo uma tabela e passando para uma função concatenada) e o HTML programático do Orbit. Eles também usam o método ´web:link´, que gera links intra-aplicação. A função ´sidebar´ usa mais funções de conveniência, para fatorar melhor:

- - - - -
 
-function _blogroll(web, blogroll) 
-local res = {} 
-for _, blog_link in ipairs(blogroll) do 
-res[#res + 1] = li(a{ href=blog_link[1], blog_link[2] }) 
-end 
-return ul(res) 
-end 
-
-function _recent(web, args) 
-local res = {} 
-for _, post in ipairs(args.recent) do 
-res[#res + 1] = li(a{ href=web:link("/post/" .. post.id), post.title }) 
-end 
-return ul(res) 
-end 
-
-function _archives(web, args) 
-local res = {} 
-for _, month in ipairs(args.months) do 
-res[#res + 1] = li(a{ href=web:link("/archive/" .. month.date_str), 
-blog.month(month) }) 
-end 
-return ul(res) 
-end 
-
- - -

Note como essas funções não chamam nada no modelo, apenas usam qualquer dado que foi passado (desde o controle).

- -

Agora podemos ir para as funções. de visualização principal. Começaremos com a mais fácil e menor, para páginas renderizadas.

- - -
 
-function render_page(web, args) 
-return layout(web, args, div.blogentry(markdown(args.page.body))) 
-end 
-
- - -

Isto é uma chamada direta para o ´layout´, passando o corpo da página dentro de um ´div´. A única coisa importante é a sintaxe ´div.blogentryp´, que gera um ´div´ com um atributo ´class´ igual ao "blogentry", ao invés de um ´div´ direto.

- -

Seguindo em frente, escreveremos a visualização para páginas index (e páginas de arquivo):

- - -
 
-function render_index(web, args) 
-if #args.posts == 0 then 
-return layout(web, args, p(strings.no_posts)) 
-else 
-local res = {} 
-local cur_time 
-for _, post in pairs(args.posts) do 
-local str_time = date(post.published_at) 
-if cur_time ~= str_time then 
-cur_time = str_time 
-res[#res + 1] = h2(str_time) 
-end 
-res[#res + 1] = h3(post.title) 
-res[#res + 1] = _post(web, post) 
-end 
-return layout(web, args, div.blogentry(res)) 
-end 
-end 
-
- - -

Novamente misturamos Lua com gerador programático, e parte fatoral do emissor (o próprio HTML para o corpo dos posts) para outra função (poderemos reutilizar esta função para visualização de apenas um post). O único pedaço incomum de lógica é o de implementar datas especiais, o código só publica quando a data muda, portanto muitos posts do mesmo dia aparecem com a mesma data.

- -

A ajuda do ´_post´ é bem simples:

- - -
 
-function _post(web, post) 
-return { 
-markdown(post.body), 
-p.posted{ 
-strings.published_at .. " " .. 
-os.date("%H:%M", post.published_at), " | ", 
-a{ href = web:link("/post/" .. post.id .. "#comments"), strings.comments .. 
-" (" .. (post.n_comments or "0") .. ")" } 
-} 
-} 
-end 
-
- - -

Agora podemos seguir para a piece-de-resistance, a visualização que renderiza posts únicos, junto com seus comentários, e o formulário "post a comment":

- - -
 
-function render_post(web, args) 
-local res = { 
-h2(span{ style="position: relative; float:left", args.post.title } 
-.. " "), 
-h3(date(args.post.published_at)), 
-_post(web, args.post) 
-} 
-res[#res + 1] = a{ name = "comments" } 
-if #args.post.comments > 0 then 
-res[#res + 1] = h2(strings.comments) 
-for _, comment in pairs(args.post.comments) do 
-res[#res + 1 ] = _comment(web, comment) 
-end 
-end 
-res[#res + 1] = h2(strings.new_comment) 
-local err_msg = "" 
-if args.comment_missing then 
-err_msg = span{ style="color: red", strings.no_comment } 
-end 
-res[#res + 1] = form{ 
-method = "post", 
-action = web:link("/post/" .. args.post.id .. "/addcomment"), 
-p{ strings.form_name, br(), input{ type="text", name="author", 
-value = web.input.author }, 
-br(), br(), 
-strings.form_email, br(), input{ type="text", name="email", 
-value = web.input.email }, 
-br(), br(), 
-strings.form_url, br(), input{ type="text", name="url", 
-value = web.input.url }, 
-br(), br(), 
-strings.comments .. ":", br(), err_msg, 
-textarea{ name="comment", rows="10", cols="60", web.input.comment }, 
-br(), 
-em(" *" .. strings.italics .. "* "), 
-strong(" **" .. strings.bold .. "** "), 
-" [" .. a{ href="/url", strings.link } .. "](http://url) ", 
-br(), br(), 
-input.button{ type="submit", value=strings.send } 
-} 
-} 
-return layout(web, args, div.blogentry(res)) 
-end 
-
- - -

São muitos códigos para se digerir de uma vez, então vamos aos poucos. As primeiras linhas geram o corpo do post, usando a ajuda ´post´. Depois temos a lista de comentários, novamente com o corpo de cada comentário gerado por uma ajuda ´comment´. No meio temos uma mensagem de erro que é gerada se o usuário tentar postar um comentário vazio, e então o formulário "add a comment". Um formulário precisa de muito HTML, então tem bastante código, mas é um HTML bem básico e auto-explicatório (torná-lo bonito é responsabilidade do style sheet).

- -

A ajuda ´_comment´ é bem simples:

- - -
 
-function _comment(web, comment) 
-return { p(comment.body), 
-p.posted{ 
-strings.written_by .. " " .. comment:make_link(), 
-" " .. strings.on_date .. " " .. 
-time(comment.created_at) 
-} 
-} 
-end 
-
- - -

Por último, precisamos configurar todas essas funções de visualização para gerador programático de HTML:

- - -
 
-orbit.htmlify(blog, "layout", "_.+", "render_.+") 
-
- - -

A função ´orbit.htmlify´ pega uma tabela e uma lista de modelos, e configura todas as funções nessa tabela com nomes que casem com um dos modelos para gerar HTML. Aqui configuraremos a função ´layout´, todas as funções ´render´, e todas as ajudas (as funções começando com ´´´).

- -

Distribuição

- -

Para esta parte do tutorial é melhor você utilizar o diretório samples/blog da distribuição Orbit (novamente, procure no diretório rocks caso você tenha instalado o Orbit via Kepler ou LuaRocks). Uma aplicação Orbit é uma aplicação WSAPI, portanto a distribuição é bastante simples, basta copiar todos os arquivos (blog.lua, blog_config.lua, blog.db, head.jpg, e style.css) para um diretório em sua raiz web (se você instalou Kepler, este seria o diretório kepler/htdocs), e criar um script disparador neste diretório. O script disparador é bem curto (chame-o de blog.ws):

- - -
 
-#!/usr/bin/env wsapi.cgi 
-require "blog" 
-return blog 
-
- - - -
 
-#!/usr/bin/env wsapi.cgi 
-require "blog" 
-return blog 
-
- - -

Dependendo de sua configuração, você pode ter que instalar os rocks luasql-sqlite3 e markdown antes de executar sua aplicação. Feito isso basta iniciar o Xavante, apontar o seu browser para blog.ws, e você deve ver a página inicial do blog. Se você criou um arquivo blog.db a partir do zero você não verá nenhum post. A aplicação de blog em `samples/blog' inclui um arquivo blog.db já contendo posts e comentários de exemplo.

- - -
- -
- - - -
- - - - diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/doc/br/example.md lua-orbit-2.2.1+dfsg/doc/br/example.md --- lua-orbit-2.2.0+gita6fb46e+dfsg/doc/br/example.md 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/doc/br/example.md 1970-01-01 00:00:00.000000000 +0000 @@ -1,564 +0,0 @@ -## Tutorial Orbit - -Este tutorial mostra como criar uma aplicação blog simples, apoiada por uma base de dados. Ela é muito simples porque não inclui nenhuma página de administração ("admin"); você tem que adicionar posts diretamente na base de dados (embora você possa postar através de um console Lua, e este tutorial mostrará como), mas tem uma interface para comentários nos posts. - -O tutorial parte do princípio que você já tem o Orbit instalado (preferencialmente como parte do [Kepler](http://www.keplerproject.org), ou pelo [LuaRocks](http://luarocks.org), e já tem um servidor web que aceita configuração WSAPI (o servidor web Xavante que vem com o Kepler é uma boa escolha). - -Os códigos fontes para este blog estão nas pastas ´samples´ da distribuição do Orbit. Se você instalou o Orbit pelo Kepler ou LuaRocks, veja dentro da pasta ´rocks´ da sua instalação. - -## Inicialização - -Você deve criar um arquivo ´blog.lua´, que será o arquivo principal de códigos fontes da nossa aplicação. A primeira coisa que você deve por no arquivo é o código para carregar o Orbit e outras bibliotecas que você usará no seu aplicativo. - -
 
-require "orbit" 
-require "orbit.cache" 
-require "markdown" 
-
- -Neste exemplo usaremos a página de  cache do Orbit, e o parser Markdown para marcar os posts. - -Todas as aplicações Orbit são módulos Lua, portanto incluiremos esta linha: - -
 
-module("blog", package.seeall, orbit.app) 
-
- -Isso configura o módulo ´blog´ e o inicializa como uma aplicação Orbit. - -´orbit.app´ coloca muitas coisas no namespace do módulo ´blog´. Os mais importantes são os metódos ´dispatch_get´, ´dispatch_post´ e ´model´ que permitem que você defina a funcionalidade principal da sua aplicação. Eles também definem a variável ´mapper´ que o Orbit usa para criar os modelos (Orbit inicializa essa variável para o seu mapeador objeto-relacional padrão). Por último, eles também definem os controles padrões para os códigos de erros 404 e 500 HTTP como as variáveis ´not_found´ e ´server_error´, respectivamente. Redefina essas variáveis se quiser páginas customizadas para a sua aplicação. - -Vamos carregar um script de configuração para o blog (um modelo comum em aplicações). Você pode pegar este script [aqui](blog_config.lua). - -
 
-require "blog_config" 
-
- -As próximas linhas carregam um driver de base de dados LuaSQL (definido na configuração), e configura o mapeador objeto relacional do Orbit. - -
 
-require("luasql." .. database.driver) 
-local env = luasql[database.driver]() 
-mapper.conn = env:connect(unpack(database.conn_data)) 
-mapper.driver = database.driver 
-
- -O mapeador do Orbit precisa usar uma conexão de base de dados, e de qual driver você estiver usando (no momento apenas o "sqlite3" e "mysql" são aceitos). - -Você precisa iniciar o mapeador antes de criar os modelos de sua aplicação porque o mapeador do Orbit consulta a base de dados durante a criação de modelos para pegar o esquema. Falando em esquema, agora é uma boa hora para criar a base de dados do seu blog. Parto do princípio que você está usando o SQLite3. Crie uma base de dados ´blog.db´ com o script SQL abaixo: - -
 
-CREATE TABLE blog_post 
-("id" INTEGER PRIMARY KEY NOT NULL, 
-"title" VARCHAR(255) DEFAULT NULL, 
-"body" TEXT DEFAULT NULL, 
-"n_comments" INTEGER DEFAULT NULL, 
-"published_at" DATETIME DEFAULT NULL); 
-
-CREATE TABLE blog_comment 
-("id" INTEGER PRIMARY KEY NOT NULL, 
-"post_id" INTEGER DEFAULT NULL, 
-"author" VARCHAR(255) DEFAULT NULL, 
-"email" VARCHAR(255) DEFAULT NULL, 
-"url" VARCHAR(255) DEFAULT NULL, 
-"body" TEXT DEFAULT NULL, 
-"created_at" DATETIME DEFAULT NULL); 
-
-CREATE TABLE blog_page 
-("id" INTEGER PRIMARY KEY NOT NULL, 
-"title" VARCHAR(30) DEFAULT NULL, 
-"body" TEXT DEFAULT NULL); 
-
- -O mapeador do Orbit usa o campo ´id´ para identificar objetos na base de dados, portanto você precisará de um para cada um dos tipos de objetos que estiver mapeando. - -Por último, vamos iniciar o cache de páginas do Orbit antes de criar nossos modelos: - -
 
-local cache = orbit.cache.new(blog, cache_path) 
-
- -O cache de páginas acelera o acesso a qualquer página que você cacheie, mas você precisará ser cuidadoso e limpar o cache para uma página quando qualquer conteúdo nela mudar. Veremos como cachear e invalidar páginas na seção de controle deste tutorial. - -## Criando Modelos - -Nossa aplicação de blog tem três tipos de objetos: posts, comentários e páginas "estáticas" (como a página de "Sobre" do blog, por exemplo). Não é coincidência que também temos três tipos de tabelas na base de dados, cada tabela mapeia um tipo de objeto que a nossa aplicação reconhece, e para cada tipo criaremos um modelo. Primeiro criaremos um objeto modelo para posts: - -
 
-posts = blog:model "post" 
-
- -O parâmetro para o método ´model´ é o nome de uma tabela na base de dados. O objeto ´posts´ que esse método cria representa a coleção de posts, e ao mesmo tempo é um protótipo para todos os posts (veremos as implicação disso em breve). O mapeador do Orbit cria um objeto funcional por conta própria: você pode fazer ´post:find(3)´, por exemplo, e pegar o post com ´id´ 3, ou ´post:find_all("n_comments < ?", { 3, order = "published_at desc"})´ e ter uma lista de todos os posts com menos de três comentários, do mais recente ao mais antigo. - -Você pode usar o método ´find´ pré-definido para todas as buscas na base de dados, mas ajuda simplificar buscas comuns nos seus métodos. Você pode fazer isso adicionando métodos no objeto ´posts´: - -
 
-function posts:find_recent() 
-return self:find_all("published_at is not null", 
-{ order = "published_at desc", 
-count = recent_count }) 
-end 
-
- -As linhas acima adicionam um método ´find_recent´ no objeto ´posts´, retornando uma lista dos posts publicados mais recentementes (o número está no script de configuração), do mais recente ao mais antigo. A aplicação irá usar este método para gerar a lista de posts na home page, assim como a seção "Posts recentes" na lateral do blog. - -Outra característica do nosso blog será a página de arquivo que mostra todos os posts de um certo mês e ano. Definiremos um método para isto também: - -
 
-function posts:find_by_month_and_year(month, year) 
-local s = os.time({ year = year, month = month, day = 1 }) 
-local e = os.time({ year = year + math.floor(month / 12), 
-month = (month % 12) + 1, 
-day = 1 }) 
-return self:find_all("published_at >= ? and published_at < ?", 
-{ s, e, order = "published_at desc" }) 
-end 
-
- -Este é o método mais complicado, já que temos que converter de um mês e ano simples para data de começo e fim no formato Lua padrão. Por último, também definiremos um método para retornar todos os meses (e anos) que tem posts, para mais tarde gerar os links para a seção "Arquivo" no sidebar: - -
 
-function posts:find_months() 
-local months = {} 
-local previous_month = {} 
-local posts = self:find_all("published_at is not null", 
-{ order = "published_at desc" }) 
-for _, post in ipairs(posts) do 
-local date = os.date("*t", post.published_at) 
-if previous_month.month ~= date.month or 
-previous_month.year ~= date.year then 
-previous_month = { month = date.month, year = date.year, 
-date_str = os.date("%Y/%m", post.published_at) } 
-months[#months + 1] = previous_month 
-end 
-end 
-return months 
-end 
-
- -Este método pega todos os posts na base de dados, ordenados por data, e iterates over them armazenando cada par de mês e ano numa lista. - -We can also define methods for individual post objects by defining methods -in the `posts` object, the only difference is how they are used (you use `find_recent` -by doing `posts:find_recent()`, but you will use `find_comments` by doing `p:find_comments()`, -where `p` is a particular post object. We will define a method to retrieve all comments of -a post: - -Também podemos definir métodos para objetos de posts individuais definindo métodos no objeto ´posts´, a única diferença é como eles são usados (você usa ´find_recent´ criando ´posts:find_recent()´, mas você usará ´find_comments´ criando ´p:find_comments()´, onde ´p´ é um objeto post específico. Definiremos um método para recuperar todos os comentários de um post: - -
 
-function posts:find_comments() 
-return comments:find_all_by_post_id{ self.id } 
-end 
-
- -Este método usa um método pré-definido do objeto ´comments´ (que criaremos em breve) que pega todos os comentários com o campo ´post_id´ iguais ao id do post atual (´self.id´). Este método cria uma relação entre os posts e os comentários; uma versão futura do mapeador Orbit permitirá que você defina isso declaradamente. - -Criar o objeto ´comments´ é simples: - -
 
-comments = blog:model "comment" 
-
- -Vamos adicionar um método de conveniência para comentários que constróem o link de comentário a partir de seus dados: - -
 
-function comments:make_link() 
-local author = self.author or strings.anonymous_author 
-if self.url and self.url ~= "" then 
-return "" .. author .. "" 
-elseif self.email and self.email ~= "" then 
-return "" .. author .. "" 
-else 
-return author 
-end 
-end 
-
- -O objeto ´pages´ é mais simples ainda, a funcionalidade padrão fornecida pelo mapeador Orbit é suficiente, então criaremos com o ´model´: - -
 
-pages = blog:model "pages" 
-
- -Isso conclui a parte "model" da nossa aplicação. Agora podemos seguir para definir a flow página da aplicação, definindo *controllers* e mapeando-os para URLs. - -## Definindo controladores - -Controladores são a interface entre a web e sua aplicação. Com o Orbit você pode -mapear a parte path das URLs de sua aplicação (pore exemplo, em http://myserver.com/myapp.ws/foo/bar -o path é /foo/bar) em controladores. Em termos Lua, um controlador Orbit é uma função que recebe -um objeto request/response (chamado `web`) e parâmetros obtidos do path, e retorna um texto -que é enviado para o cliente (geralmente HTML, mas pode ser XML ou mesmo uma imagem). - -Você mapeia paths em controladores com os métodos `dispatch_get` e `dispatch_post` -para requisições GET e POST respectivamente. O primeiro parâmetro destes métodos é o -controlador, uma função Lua, e todos os outros parâmetros são os *padrões de mapeamento*, -escritos na sintaxe de padrões de strings de Lua, de forma que um controlador pode responder -a mapeamentos diversos. - -Abaixo esta o controlador para a pagina principal do blog: - -
 
-function index(web) 
-local ps = posts:find_recent() 
-local ms = posts:find_months() 
-local pgs = pgs or pages:find_all() 
-return render_index(web, { posts = ps, months = ms, 
-recent = ps, pages = pgs }) 
-end 
-
-blog:dispatch_get(cache(index), "/", "/index") 
-
- -A última linha estabelece o mapeamento entre a função `index` e root da aplicação. -O chamado do `cache` define o *caching* para esse controlador, -usando o *cache* que criamos anteriormente (este é outro idioma comum do Lua, -funciona como"decorators"). - -O controlador `index`mostra todos os posts recentes, e é bem direto. Ele somente chama os dados modelo solicitados apartir do banco de dados, depois chama uma função auxiliar (chamada *view* na tecnologia MVC) para  renderizar o código HTML atual. - -Outro importante controlador é o que mostra posts únicos: - -
 
-function view_post(web, post_id, comment_missing) 
-local post = posts:find(tonumber(post_id)) 
-if post then 
-local recent = posts:find_recent() 
-local pgs = pages:find_all() 
-post.comments = post:find_comments() 
-local months = posts:find_months() 
-return render_post(web, { post = post, months = months, 
-recent = recent, pages = pgs, 
-comment_missing = comment_missing }) 
-else 
-return not_found(web) 
-end 
-end 
-
-blog:dispatch_get(cache(view_post), "/post/(%d+)") 
-
- -Aqui nós mapeamos todos os paths como /post/53 para o controlador `view_post`. O pattern captura os números, e é passado por todo controlador pelo Orbit. Para /post/53, o controlador recebe a string ''53'' como `post_id` e usa isto para chamar o post correspondente. Novamente, a renderização do HTML esta factored out para outra função, e este controlador esta cached. - -Se nenhum post com este id for encontrado, o controlador default de páginas perdidas será chamado `blog.not_found` (`orbit.app` coloque isto no  namespace do `blog`) - -Arquivos e paginas tem estruturas similares: - - -
 
-function view_archive(web, year, month) 
-local ps = posts:find_by_month_and_year(tonumber(month), 
-tonumber(year)) 
-local months = posts:find_months() 
-local recent = posts:find_recent() 
-local pgs = pages:find_all() 
-return render_index(web, { posts = ps, months = months, 
-recent = recent, pages = pgs }) 
-end 
-
-blog:dispatch_get(cache(view_archive), "/archive/(%d%d%d%d)/(%d%d)") 
-
-function view_page(web, page_id) 
-local page = pages:find(tonumber(page_id)) 
-if page then 
-local recent = posts:find_recent() 
-local months = posts:find_months() 
-local pgs = pages:find_all() 
-return render_page(web, { page = page, months = months, 
-recent = recent, pages = pgs }) 
-else 
-not_found(web) 
-end 
-end 
-
-blog:dispatch_get(cache(view_page), "/page/(%d+)") 
-
- -Os arquivos utilizam o mesmo *layout* que o index, logo ele reutiliza seu gerador de HTML. Os arquivos também extraem dois parametros do *path*, o mês e o ano, logo os *paths* são como /archive/2008/05. - -Finalmente você pode também pode definir arquivos estáticos com o método de conveniência `dispatch_static`. - -
 
-blog:dispatch_static("/head%.jpg", "/style%.css") 
-
- -Esses também são *patterns*, logo os pontos são escapados. Você pode definir em sua aplicação, uma pasta como estática com `blog:dispatch_static("/templates/.+")`. O Orbit sempre procura pelos arquivos nas pastas das aplicações. Claro que você pode deixar sua aplicação comportar somente conteúdos dinâmicos e deixar seu servidor web servir conteúdo estátic; `dispatch_static` é só uma conveniência para ter aplicações "zero-configuration" - -Tem um controlador para adicionar comentários. Este irá responder ao POST em vez de receber: - -
 
-function add_comment(web, post_id) 
-local input = web.input 
-if string.find(input.comment, "^%s*$") then 
-return view_post(web, post_id, true) 
-else 
-local comment = comments:new() 
-comment.post_id = tonumber(post_id) 
-comment.body = markdown(input.comment) 
-if not string.find(input.author, "^%s*$") then 
-comment.author = input.author 
-end 
-if not string.find(input.email, "^%s*$") then 
-comment.email = input.email 
-end 
-if not string.find(input.url, "^%s*$") then 
-comment.url = input.url 
-end 
-comment:save() 
-local post = posts:find(tonumber(post_id)) 
-post.n_comments = (post.n_comments or 0) + 1 
-post:save() 
-cache:invalidate("/") 
-cache:invalidate("/post/" .. post_id) 
-cache:invalidate("/archive/" .. os.date("%Y/%m", post.published_at)) 
-return web:redirect(web:link("/post/" .. post_id)) 
-end 
-end 
-
-blog:dispatch_post(add_comment, "/post/(%d+)/addcomment") 
-
- -O controlador `add_comment` primeiro valida o input, delegando ao `view_post` se o campo de comentário estiver vazio (o qual irá mostrar uma menssagem de erro na página). Você acessa o parâmetro POST pela tabela `web.input`, que é convenientemente  aliased para um `input` local variável.  - -O controlador cria um novo objeto comment, preenche com dados e depois salva no banco de dados. Ele também atualiza o objeto post para aumentar o número de *comment* o post tem por um, alem de salvar. Depois continua para invalidar (em *cache*) todas as páginas que talvez mostre esta informação: o index, a página de postagem e os arquivos para este post em particular. Finalmente, ele redireciona para a página de postagem, que irá mostrar o novo comentário. Este é um idioma comum na programação web chamada POST-REDIRECT-GET, onde todo POST é seguido por um redirecionamente para um GET. Isso evita a dupla postagem no caso do usuário carregar a página novamente. - -A única coisa que resta agora é a geração de HTML. Está é um tópico da próxima sessão. - -## Visualizações: Gerando HTML - -Visualizações é o último componente do trio MVC. Para o Orbit, visualizações são funções simples que geram conteúdo (geralmente HTML), e são estritamente opcionais, o que significa que você pode devolver conteúdo diretamente do seu controle. Mas ainda é bom ter prática em programação para separar controles e visualizações. - -Como você gera conteúdo é escolha sua: concatene correntes Lua, use ´table.concat´, use um template de biblioteca de terceiros... Orbit fornece geração de HTML/XML programático através de órbit.htmlify´, mas você está livre para usar qualquer método que preferir. Neste tutorial manteremos a geração programática, embora, assim como outros métodos (strings retas, [Cosmo](http://cosmo.luaforge.net), etc.) são inteiramente documentadas em outro lugar. - -Quando você htmlify uma função, o Orbit muda o ambiente da função permitindo que você gere HTML chamando as tags de funções. É melhor mostrar como funciona do que explicar, então aqui vai a visualização básica da aplicação do blog, ´layout´: - -
 
-function layout(web, args, inner_html) 
-return html{ 
-head{ 
-title(blog_title), 
-meta{ ["http-equiv"] = "Content-Type", 
-content = "text/html; charset=utf-8" }, 
-link{ rel = 'stylesheet', type = 'text/css', 
-href = web:static_link('/style.css'), media = 'screen' } 
-}, 
-body{ 
-div{ id = "container", 
-div{ id = "header", title = "sitename" }, 
-div{ id = "mainnav", 
-_menu(web, args) 
-}, 
-div{ id = "menu", 
-_sidebar(web, args) 
-}, 
-div{ id = "contents", inner_html }, 
-div{ id = "footer", copyright_notice } 
-} 
-} 
-} 
-end 
-
- -Esta visualização é um decorador para outras visualizações, e gera o boilerplate para cada página do blog (cabeçalho, rodapé, sidebar). Você pode ver as funções de gerador HTML por todo o código, como ´title´, ´html´, ´head´, ´div´. Cada um tem ou uma string ou uma tabela, e gera o HTML correspondente. Se você dispensar uma tabela, a parte de banco de dados é concatenada e usada como conteúdo, enquanto a parte hash é usada como atributos HTML para aquela tag. Uma tag sem conteúdo gera um tag self-closing (´meta´ e ´link´ no código acima). - -Digno de nota no código acima são as chamadas ´web:static_link´ e as funções ´_menu´ e ´_sidebar´.O método ´static_link´ gera um link para um recurso estático da aplicação, tirando o SCRIPT_NAME da URL (por exemplo, se a URL é http://myserver.com/myblog/blog.ws/index irá voltar como /myblog/style.css como o link). - -As funções ´_menu´ e ´_sidebar´ são apenas visualizações de ajuda para gerar a barra de menu e sidebar do blog: - -
 
-function _menu(web, args) 
-local res = { li(a{ href= web:link("/"), strings.home_page_name }) } 
-for _, page in pairs(args.pages) do 
-res[#res + 1] = li(a{ href = web:link("/page/" .. page.id), page.title }) 
-end 
-return ul(res) 
-end 
-
-function _sidebar(web, args) 
-return { 
-h3(strings.about_title), 
-ul(li(about_blurb)), 
-h3(strings.last_posts), 
-_recent(web, args), 
-h3(strings.blogroll_title), 
-_blogroll(web, blogroll), 
-h3(strings.archive_title), 
-_archives(web, args) 
-} 
-end 
-
- -Aqui você vê uma mistura de idiomas básicos do Lua (preenchendo uma tabela e passando para uma função concatenada) e o HTML programático do Orbit. Eles também usam o método ´web:link´, que gera links intra-aplicação. A função ´sidebar´ usa mais funções de conveniência, para fatorar melhor: - - - -
 
-function _blogroll(web, blogroll) 
-local res = {} 
-for _, blog_link in ipairs(blogroll) do 
-res[#res + 1] = li(a{ href=blog_link[1], blog_link[2] }) 
-end 
-return ul(res) 
-end 
-
-function _recent(web, args) 
-local res = {} 
-for _, post in ipairs(args.recent) do 
-res[#res + 1] = li(a{ href=web:link("/post/" .. post.id), post.title }) 
-end 
-return ul(res) 
-end 
-
-function _archives(web, args) 
-local res = {} 
-for _, month in ipairs(args.months) do 
-res[#res + 1] = li(a{ href=web:link("/archive/" .. month.date_str), 
-blog.month(month) }) 
-end 
-return ul(res) 
-end 
-
- -Note como essas funções não chamam nada no modelo, apenas usam qualquer dado que foi passado (desde o controle). - -Agora podemos ir para as funções. de visualização principal. Começaremos com a mais fácil e menor, para páginas renderizadas. - -
 
-function render_page(web, args) 
-return layout(web, args, div.blogentry(markdown(args.page.body))) 
-end 
-
- -Isto é uma chamada direta para o ´layout´, passando o corpo da página dentro de um ´div´. A única coisa importante é a sintaxe ´div.blogentryp´, que gera um ´div´ com um atributo ´class´ igual ao "blogentry", ao invés de um ´div´ direto. - -Seguindo em frente, escreveremos a visualização para páginas index (e páginas de arquivo): - -
 
-function render_index(web, args) 
-if #args.posts == 0 then 
-return layout(web, args, p(strings.no_posts)) 
-else 
-local res = {} 
-local cur_time 
-for _, post in pairs(args.posts) do 
-local str_time = date(post.published_at) 
-if cur_time ~= str_time then 
-cur_time = str_time 
-res[#res + 1] = h2(str_time) 
-end 
-res[#res + 1] = h3(post.title) 
-res[#res + 1] = _post(web, post) 
-end 
-return layout(web, args, div.blogentry(res)) 
-end 
-end 
-
- -Novamente misturamos Lua com gerador programático, e parte fatoral do emissor (o próprio HTML para o corpo dos posts) para outra função (poderemos reutilizar esta função para visualização de apenas um post). O único pedaço incomum de lógica é o de implementar datas especiais, o código só publica quando a data muda, portanto muitos posts do mesmo dia aparecem com a mesma data. - -A ajuda do ´_post´ é bem simples: - -
 
-function _post(web, post) 
-return { 
-markdown(post.body), 
-p.posted{ 
-strings.published_at .. " " .. 
-os.date("%H:%M", post.published_at), " | ", 
-a{ href = web:link("/post/" .. post.id .. "#comments"), strings.comments .. 
-" (" .. (post.n_comments or "0") .. ")" } 
-} 
-} 
-end 
-
- -Agora podemos seguir para a piece-de-resistance, a visualização que renderiza posts únicos, junto com seus comentários, e o formulário "post a comment": - -
 
-function render_post(web, args) 
-local res = { 
-h2(span{ style="position: relative; float:left", args.post.title } 
-.. " "), 
-h3(date(args.post.published_at)), 
-_post(web, args.post) 
-} 
-res[#res + 1] = a{ name = "comments" } 
-if #args.post.comments > 0 then 
-res[#res + 1] = h2(strings.comments) 
-for _, comment in pairs(args.post.comments) do 
-res[#res + 1 ] = _comment(web, comment) 
-end 
-end 
-res[#res + 1] = h2(strings.new_comment) 
-local err_msg = "" 
-if args.comment_missing then 
-err_msg = span{ style="color: red", strings.no_comment } 
-end 
-res[#res + 1] = form{ 
-method = "post", 
-action = web:link("/post/" .. args.post.id .. "/addcomment"), 
-p{ strings.form_name, br(), input{ type="text", name="author", 
-value = web.input.author }, 
-br(), br(), 
-strings.form_email, br(), input{ type="text", name="email", 
-value = web.input.email }, 
-br(), br(), 
-strings.form_url, br(), input{ type="text", name="url", 
-value = web.input.url }, 
-br(), br(), 
-strings.comments .. ":", br(), err_msg, 
-textarea{ name="comment", rows="10", cols="60", web.input.comment }, 
-br(), 
-em(" *" .. strings.italics .. "* "), 
-strong(" **" .. strings.bold .. "** "), 
-" [" .. a{ href="/url", strings.link } .. "](http://url) ", 
-br(), br(), 
-input.button{ type="submit", value=strings.send } 
-} 
-} 
-return layout(web, args, div.blogentry(res)) 
-end 
-
- -São muitos códigos para se digerir de uma vez, então vamos aos poucos. As primeiras linhas geram o corpo do post, usando a ajuda ´_post´. Depois temos a lista de comentários, novamente com o corpo de cada comentário gerado por uma ajuda ´_comment´. No meio temos uma mensagem de erro que é gerada se o usuário tentar postar um comentário vazio, e então o formulário "add a comment". Um formulário precisa de muito HTML, então tem bastante código, mas é um HTML bem básico e auto-explicatório (torná-lo bonito é responsabilidade do style sheet). - -A ajuda ´_comment´ é bem simples: - -
 
-function _comment(web, comment) 
-return { p(comment.body), 
-p.posted{ 
-strings.written_by .. " " .. comment:make_link(), 
-" " .. strings.on_date .. " " .. 
-time(comment.created_at) 
-} 
-} 
-end 
-
- -Por último, precisamos configurar todas essas funções de visualização para gerador programático de HTML: - -
 
-orbit.htmlify(blog, "layout", "_.+", "render_.+") 
-
- -A função ´orbit.htmlify´ pega uma tabela e uma lista de modelos, e configura todas as funções nessa tabela com nomes que casem com um dos modelos para gerar HTML. Aqui configuraremos a função ´layout´, todas as funções ´render_´, e todas as ajudas (as funções começando com ´_´´). - -## Distribuição - -Para esta parte do tutorial é melhor você utilizar o diretório `samples/blog` da distribuição Orbit (novamente, procure no diretório `rocks` caso você tenha instalado o Orbit via Kepler ou LuaRocks). Uma aplicação Orbit é uma aplicação WSAPI, portanto a distribuição é bastante simples, basta copiar todos os arquivos (`blog.lua`, `blog_config.lua`, `blog.db`, `head.jpg`, e `style.css`) para um diretório em sua raiz web (se você instalou Kepler, este seria o diretório `kepler/htdocs`), e criar um script disparador neste diretório. O script disparador é bem curto (chame-o de `blog.ws`): - -
 
-#!/usr/bin/env wsapi.cgi 
-require "blog" 
-return blog 
-
- -
 
-#!/usr/bin/env wsapi.cgi 
-require "blog" 
-return blog 
-
- -Dependendo de sua configuração, você pode ter que instalar os rocks `luasql-sqlite3` e `markdown` antes de executar sua aplicação. Feito isso basta iniciar o Xavante, apontar o seu browser para blog.ws, e você deve ver a página inicial do blog. Se você criou um arquivo blog.db a partir do zero você não verá nenhum post. A aplicação de blog em `samples/blog' inclui um arquivo blog.db já contendo posts e comentários de exemplo. \ No newline at end of file diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/doc/br/index.html lua-orbit-2.2.1+dfsg/doc/br/index.html --- lua-orbit-2.2.0+gita6fb46e+dfsg/doc/br/index.html 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/doc/br/index.html 1970-01-01 00:00:00.000000000 +0000 @@ -1,209 +0,0 @@ - - - - - Orbit - - - - - - -
- -
- - -
Orbit
-
MVC desenvolvimento Web em Lua
-
- -
- - - -
- - -

Apresentação

- -

O Orbit é um framework MVC web para a linguagem Lua. O design é ispirado por framework leves em Ruby como o Camping. Abandona completamente o modelo atual de CGILua que usa "scripts" em favor de aplicações, onde cada aplicação do Orbit pode caber em um único arquivo, mas se você quiser, pode dividi-lo em múltiplos arquivos. Todos os aplicativos do Orbit seguem o protocolo WSAPI, por tanto atualmente trabalha com Xavante, CGI e Fastcgi. Isto inclui um disparador que torna mais fácil iniciar uma instância do Xavante para o desenvolvimento.

- -

Histórico

- -
    -
  • Versão 2.0.1: consertado o bug na função redirect nas Orbit pages (agradecimentos a Ignacio Burgueño por ter encontrado o bug )

  • -
  • Versão 2.0: Revisão completa do Orbit

  • -
  • Versão 1.0: Versão inicial, obsoleta

  • -
- -

Hello World

- -

Segue uma aplicação muito simples do Orbit:

- - -
-#!/usr/bin/env wsapi.cgi
-
-require"orbit"
-
--- As aplicações do Orbit normalmente são módulos,
--- o orbit.new faz as inicializações necessárias
-
-module("hello", package.seeall, orbit.new)
-
--- Esses são os controladores, cada um recebe um objeto web correspondente 
--- ao request/response, mais qualquer capturas extras dos
--- padrões de dispatch. O controlador envia qualquer cabeçalho extra e/ou
--- o status, caso não seja 200, então retona a resposta. 
--- É recomendado delegar a geração das respostas para um função
--- de visualização.
-
-function index(web)
-return render_index()
-end
-
-function say(web, name)
-return render_say(web, name)
-end
-
--- Constrói a tabela de dispatch das aplicações, você pode 
--- passar multiplos padrões, e todas as capturas são repassadas 
--- ao controlador
-
-hello:dispatch_get(index, "/", "/index")
-hello:dispatch_get(say, "/say/(%a+)")
-
--- Essas são funções de visualização as quais os controladores fazem refência.
--- orbit.htmlify percorre as funções na tabela passada como primeiro parâmetro
--- e tenta combinar seus nomes com os padrões passados (usando um ^ e um $
--- implícitos ao redor do padrão). Cada função que corresponde ao padrão recebe
--- um ambiente onde funções de geração de HTML são geradas por demanda.
--- Estas funções aceitam nil (tags vazias), uma string (texto presente entre
--- a tag de abertura e a de fechamento), ou uma tabela com atributos e uma lista
--- de strings que correspondem ao texto. A indexação das funções adiciona um atributo
--- de classe ao tag. Funções são cacheadas.
-
--- Função de apoio para as partes comuns de uma página
-
-function render_layout(inner_html)
-return html{
-head{ title"Hello" },
-body{ inner_html }
-}
-end
-
-function render_hello()
-return p.hello"Hello World!"
-end
-function render_index()
-return render_layout(render_hello())
-end
-
-function render_say(web, name)
-return render_layout(render_hello() .. 
-p.hello((web.input.greeting or "Hello ") .. name .. "!"))
-end
-
-orbit.htmlify(hello, "render_.+")
-
-return _M
-
- - -

O exemplo utiliza a geração de HTML nativa do Orbit, mas sinta-se a vontade para utilizar qualquer método de geração de HTML. -Por exemplo, um dos exemplos de aplicação do Orbit utiliza a biblioteca de template Cosmo

- -

Mapeamento OR

- -

O Orbit inclui um mapeador objeto-relacional básico que no momento funciona somente com  drivers -SQLite3 e MySQL do LuaSQL's. O mapeador fornece um método dinâmico de busca 'como o ActiveRecord de Rails (find_by_field1_and_field2{val1, val2}), bem como templates para condições (find_by("field1 = ? or field1 = ?", { val1, val2 })). A aplicação de exemplo utiliza esse mapeador.

- -

Um bom efeito collateral do modelo de aplicação do Orbit é que nós conseguimos um "console de aplicações" de graça. No caso do blog, por exemplo, podemos adicionar uma nova postagem usando:

- - -
-$ lua -l luarocks.require -i blog.lua
-> p = blog.posts:new()
-> p.title = "Novo post"
-> p.body = "Este é um novo post. Use *Markdown* se preferir."
-> p.published_at = os.time()
-> p:save()
-
- - -

Você também pode atualizar ou deletar qualquer itens do modelo direto do seu console, bastanto obte-los do banco de dados, mudar o que você quer e chamar save()(ou delete() se você quiser remove-lo).

- -

Download e instalação

- -

A forma mais fácil de baixar e instalar o Orbit é atraves do LuaRocks. Você pode instalar o Orbit com o simples comando luarocks install orbit Vá para o path onde o LuaRocks coloca o Orbit para ver as aplicações e esta documentação. O Luarocks obtem e instala qualquer qualquer dependência que você ainda não tenha.

- -

Você também pode obter o Orbit no site do LuaForge. A instalação em sistemas Unix e similares é "configure && make && make install", mas você pode ter que instalar algumas dependências sozinho (como o WSAPI e o Xavante).

- -

Créditos

- -

O Orbit foi projetado e desenvolvido por Fabio Mascarenhas and André Carregal e é mantido por Fabio Mascarenhas. Orbit foi patrocinado pela Fábrica Digital, FINEP e SEBRAE

- -

Fale conosco

- -

Para maiores informações, por favor [fale conosco] (mailto:info-NO-SPAM-THANKS@keplerproject.org). Comentários são bem-vindos.

- -

Você também pode nos contactar e a outros desenvolvedores e usuários na lista de discussão do projeto Kepler.

- - - - - -
- -
- - - -
- - - - diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/doc/br/index.md lua-orbit-2.2.1+dfsg/doc/br/index.md --- lua-orbit-2.2.0+gita6fb46e+dfsg/doc/br/index.md 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/doc/br/index.md 1970-01-01 00:00:00.000000000 +0000 @@ -1,122 +0,0 @@ -##Apresentação - -O Orbit é um framework MVC web para a linguagem Lua. O design é ispirado por framework leves em Ruby como o [Camping](http://code.whytheluckystiff.net/camping/). Abandona completamente o modelo atual de CGILua que usa "scripts" em favor de aplicações, onde cada aplicação do Orbit pode caber em um único arquivo, mas se você quiser, pode dividi-lo em múltiplos arquivos. Todos os aplicativos do Orbit seguem o protocolo [WSAPI](http://wsapi.luaforge.net), por tanto atualmente trabalha com Xavante, CGI e Fastcgi. Isto inclui um disparador que torna mais fácil iniciar uma instância do Xavante para o desenvolvimento. - -##Histórico - -* Versão 2.0.1: consertado o bug na função redirect nas Orbit pages (agradecimentos a Ignacio Burgueño por ter encontrado o bug ) - -* Versão 2.0: Revisão completa do Orbit - -* Versão 1.0: Versão inicial, obsoleta - -## Hello World - -Segue uma aplicação muito simples do Orbit: - -
-#!/usr/bin/env wsapi.cgi
-
-require"orbit"
-
--- As aplicações do Orbit normalmente são módulos,
--- o orbit.new faz as inicializações necessárias
-
-module("hello", package.seeall, orbit.new)
-
--- Esses são os controladores, cada um recebe um objeto web correspondente 
--- ao request/response, mais qualquer capturas extras dos
--- padrões de dispatch. O controlador envia qualquer cabeçalho extra e/ou
--- o status, caso não seja 200, então retona a resposta. 
--- É recomendado delegar a geração das respostas para um função
--- de visualização.
-
-function index(web)
-return render_index()
-end
-
-function say(web, name)
-return render_say(web, name)
-end
-
--- Constrói a tabela de dispatch das aplicações, você pode 
--- passar multiplos padrões, e todas as capturas são repassadas 
--- ao controlador
-
-hello:dispatch_get(index, "/", "/index")
-hello:dispatch_get(say, "/say/(%a+)")
-
--- Essas são funções de visualização as quais os controladores fazem refência.
--- orbit.htmlify percorre as funções na tabela passada como primeiro parâmetro
--- e tenta combinar seus nomes com os padrões passados (usando um ^ e um $
--- implícitos ao redor do padrão). Cada função que corresponde ao padrão recebe
--- um ambiente onde funções de geração de HTML são geradas por demanda.
--- Estas funções aceitam nil (tags vazias), uma string (texto presente entre
--- a tag de abertura e a de fechamento), ou uma tabela com atributos e uma lista
--- de strings que correspondem ao texto. A indexação das funções adiciona um atributo
--- de classe ao tag. Funções são cacheadas.
-
--- Função de apoio para as partes comuns de uma página
-
-function render_layout(inner_html)
-return html{
-head{ title"Hello" },
-body{ inner_html }
-}
-end
-
-function render_hello()
-return p.hello"Hello World!"
-end
-function render_index()
-return render_layout(render_hello())
-end
-
-function render_say(web, name)
-return render_layout(render_hello() .. 
-p.hello((web.input.greeting or "Hello ") .. name .. "!"))
-end
-
-orbit.htmlify(hello, "render_.+")
-
-return _M
-
- -O exemplo utiliza a geração de HTML nativa do Orbit, mas sinta-se a vontade para utilizar qualquer método de geração de HTML. -Por exemplo, um dos exemplos de aplicação do Orbit utiliza a biblioteca de template [Cosmo](http://cosmo.luaforge.net) - -## Mapeamento OR - -O Orbit inclui um mapeador objeto-relacional básico que no momento funciona somente com  drivers -SQLite3 e MySQL do [LuaSQL's](http://luaforge.net/projects/luasql). O mapeador fornece um método dinâmico de busca 'como o ActiveRecord de Rails (find\_by\_field1\_and\_field2{val1, val2}), bem como templates para condições (find_by("field1 = ? or field1 = ?", { val1, val2 })). A aplicação de exemplo utiliza esse mapeador. - -Um bom efeito collateral do modelo de aplicação do Orbit é que nós conseguimos um "console de aplicações" de graça. No caso do blog, por exemplo, podemos adicionar uma nova postagem usando: - -
-$ lua -l luarocks.require -i blog.lua
-> p = blog.posts:new()
-> p.title = "Novo post"
-> p.body = "Este é um novo post. Use *Markdown* se preferir."
-> p.published_at = os.time()
-> p:save()
-
- -Você também pode atualizar ou deletar qualquer itens do modelo direto do seu console, bastanto obte-los do banco de dados, mudar o que você quer e chamar `save()`(ou `delete()` se você quiser remove-lo). - -## Download e instalação - -A forma mais fácil de baixar e instalar o Orbit é atraves do [LuaRocks](http://luarocks.org). Você pode instalar o Orbit com o simples comando `luarocks install orbit` Vá para o path onde o LuaRocks coloca o Orbit para ver as aplicações e esta documentação. O Luarocks obtem e instala qualquer qualquer dependência que você ainda não tenha. - -Você também pode obter o Orbit no site do [LuaForge](http://luaforge.net/projects/orbit). A instalação em sistemas Unix e similares é "configure && make && make install", mas você pode ter que instalar algumas dependências sozinho (como o WSAPI e o Xavante). - -## Créditos - -O Orbit foi projetado e desenvolvido por Fabio Mascarenhas and André Carregal e é mantido por Fabio Mascarenhas. Orbit foi patrocinado pela Fábrica Digital, FINEP e SEBRAE - -## Fale conosco - -Para maiores informações, por favor [fale conosco] (mailto:info-NO-SPAM-THANKS@keplerproject.org). Comentários são bem-vindos. - -Você também pode nos contactar e a outros desenvolvedores e usuários na [lista de discussão](http://luaforge.net/mail/?group_id=104) do projeto Kepler. - - diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/doc/br/license.html lua-orbit-2.2.1+dfsg/doc/br/license.html --- lua-orbit-2.2.0+gita6fb46e+dfsg/doc/br/license.html 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/doc/br/license.html 1970-01-01 00:00:00.000000000 +0000 @@ -1,97 +0,0 @@ - - - - - Orbit - - - - - - -
- -
- - -
Orbit
-
MVC desenvolvimento Web em Lua
-
- -
- - - -
- - - -

Orbit é um software livre: ele pode ser usado para fins acadêmicos e comerciais sem custo algum. Não tem royalties, nem restrições de “copyleft” como a GNU. Orbit é um software open source. Sua licença é compatível com GPL. Orbit não é de domínio público e o Projeto

- -

Orbit mantém seu copyright. Os detalhes legais seguem abaixo. -A idéia da licença é que você possa utilizar o Orbit livremente para qualquer finalidade, sem custo algum e sem precisar pedir permissão. O único requisito é que você nos dê o crédito caso utilize o Orbit, incluindo a notificação de copyright apropriada em algum lugar de seu produto ou documentação.

- -

Orbit é desenvolvido e implementado pela equipe do Orbit. A implementação não é derivada de um software licenciado.

- - -

Copyright © 2004 Projeto Kepler.

- -
- -

Permissão é aqui dada, livre de cobranças, a qualquer pessoa obtendo uma cópia deste programa e de seus arquivos de documentação (o "Programa"), para distribuí-lo sem restrição, incluindo sem limitações os direitos de usar, copiar, modificar, associar, publicar, distribuir, sub-licenciar e/ou vender cópias do mesmo, e para permitir a pessoas a quem o Programa for fornecido os mesmos direitos, sujeitos somente às seguintes condições:

- -

A nota de copyright acima e esta nota de permissão devem ser incluídas em todas as cópias ou porções substanciais do Programa.

- -

O PROGRAMA É FORNECIDO TAL COMO ESTÁ, SEM GARANTIAS DE QUALQUER ESPÉCIE, EXPRESSAS OU IMPLÍCITAS, INCLUÍDAS MAS NÃO LIMITADAS ÀS GARANTIAS MERCANTIS, ÀS CAPAZES DE SATISFAZER INTERESSES PARTICULARES E ÀS DE NÃO INFRIGIMENTO DOS DIREITOS DE TERCEIROS. EM NENHUMA HIPÓTESE OS AUTORES OU DETENTORES DO COPYRIGHT DESTE PROGRAMA PODEM SER RESPONSABILIZADOS POR QUALQUER ACUSAÇÃO, SEJA DE DANOS, ESTRAGOS OU PREJUÍZOS, MESMO QUANDO PREVISTO EM CONTRATO OU EM QUALQUER OUTRO ARTIFÍCIO LEGAL, DECORRENTE OU EM CONEXÃO COM O USO DESTE PROGRAMA OU DE SUAS DISTRIBUIÇÕES.

- - - - -
- -
- - - -
- - - - diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/doc/br/license.md lua-orbit-2.2.1+dfsg/doc/br/license.md --- lua-orbit-2.2.0+gita6fb46e+dfsg/doc/br/license.md 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/doc/br/license.md 1970-01-01 00:00:00.000000000 +0000 @@ -1,19 +0,0 @@ - -Orbit é um software livre: ele pode ser usado para fins acadêmicos e comerciais sem custo algum. Não tem royalties, nem restrições de “copyleft” como a GNU. Orbit é um software open source. Sua licença é compatível com [GPL](http://www.gnu.org/copyleft/gpl.html). Orbit não é de domínio público e o Projeto - -Orbit mantém seu copyright. Os detalhes legais seguem abaixo. -A idéia da licença é que você possa utilizar o Orbit livremente para qualquer finalidade, sem custo algum e sem precisar pedir permissão. O único requisito é que você nos dê o crédito caso utilize o Orbit, incluindo a notificação de *copyright* apropriada em algum lugar de seu produto ou documentação. - -Orbit é desenvolvido e implementado pela equipe do Orbit. A implementação não é derivada de um software licenciado. - - -Copyright © 2004 Projeto Kepler. - ------------ - -Permissão é aqui dada, livre de cobranças, a qualquer pessoa obtendo uma cópia deste programa e de seus arquivos de documentação (o "Programa"), para distribuí-lo sem restrição, incluindo sem limitações os direitos de usar, copiar, modificar, associar, publicar, distribuir, sub-licenciar e/ou vender cópias do mesmo, e para permitir a pessoas a quem o Programa for fornecido os mesmos direitos, sujeitos somente às seguintes condições: - -A nota de *copyright* acima e esta nota de permissão devem ser incluídas em todas as cópias ou porções substanciais do Programa. - -O PROGRAMA É FORNECIDO TAL COMO ESTÁ, SEM GARANTIAS DE QUALQUER ESPÉCIE, EXPRESSAS OU IMPLÍCITAS, INCLUÍDAS MAS NÃO LIMITADAS ÀS GARANTIAS MERCANTIS, ÀS CAPAZES DE SATISFAZER INTERESSES PARTICULARES E ÀS DE NÃO INFRIGIMENTO DOS DIREITOS DE TERCEIROS. EM NENHUMA HIPÓTESE OS AUTORES OU DETENTORES DO COPYRIGHT DESTE PROGRAMA PODEM SER RESPONSABILIZADOS POR QUALQUER ACUSAÇÃO, SEJA DE DANOS, ESTRAGOS OU PREJUÍZOS, MESMO QUANDO PREVISTO EM CONTRATO OU EM QUALQUER OUTRO ARTIFÍCIO LEGAL, DECORRENTE OU EM CONEXÃO COM O USO DESTE PROGRAMA OU DE SUAS DISTRIBUIÇÕES. - diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/doc/br/makedoc.lua lua-orbit-2.2.1+dfsg/doc/br/makedoc.lua --- lua-orbit-2.2.0+gita6fb46e+dfsg/doc/br/makedoc.lua 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/doc/br/makedoc.lua 1970-01-01 00:00:00.000000000 +0000 @@ -1,114 +0,0 @@ -local cosmo = require "cosmo" -require "markdown" - -local pages = { - { name = "Home", file = "index", sections = {} }, - { name = "Pages", file = "pages", sections = {} }, - { name = "Referência", file = "reference", sections = {} }, - { name = "Tutorial", file = "example", sections = {} }, - { name = "Licença", file = "license", sections = {} } -} - -local project = { - name = "Orbit", - blurb = "MVC desenvolvimento Web em Lua", - logo = "orbit.png", -} - -local template = [==[ - - - - - $name - - - - - - -
- -
- - -
$name
-
$blurb
-
- -
- - - -
- -$content - -
- -
- - - -
- - - - -]==] - -local function readfile(filename) - local file = io.open(filename) - local contents = file:read("*a") - file:close() - return contents -end - -local function writefile(filename, contents) - local file = io.open(filename, "w+") - file:write(contents) - file:close() -end - -local function gen_page(project, pages, p) - project.pages = function () - for _, page in ipairs(pages) do - local namelink - if page.file == p.file then - namelink = cosmo.fill([[$name]], { name = page.name}) - else - namelink = cosmo.fill([[$name]], { name = page.name, file = page.file}) - end - cosmo.yield{ namelink = namelink, sections = function () - for _, s in ipairs(page.sections) do - cosmo.yield{ name = s.name, anchor = - page.file .. ".html#" .. s.anchor } - end - end } - end - end - return (cosmo.fill(template, project)) -end - -for _, p in ipairs(pages) do - project.content = markdown(readfile(p.file .. ".md")) - writefile(p.file .. ".html", gen_page(project, pages, p)) -end Binary files /tmp/pE0qlHJ2Sm/lua-orbit-2.2.0+gita6fb46e+dfsg/doc/br/orbit.png and /tmp/2_uePaVKcF/lua-orbit-2.2.1+dfsg/doc/br/orbit.png differ diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/doc/br/pages.html lua-orbit-2.2.1+dfsg/doc/br/pages.html --- lua-orbit-2.2.0+gita6fb46e+dfsg/doc/br/pages.html 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/doc/br/pages.html 1970-01-01 00:00:00.000000000 +0000 @@ -1,163 +0,0 @@ - - - - - Orbit - - - - - - -
- -
- - -
Orbit
-
MVC desenvolvimento Web em Lua
-
- -
- - - -
- - -

Orbit Pages

- -

Orbit Pages é um ambiente de programação similar ao PHP, construido sobre o Orbit. -As Orbit Pages são páginas HTML (mas que utilizam a extensão .op em uma instalação do Orbit típica) -que são convertidas dinamicamente em uma aplicação Orbit. -Elas são inicializadas pelos disparadores op.cgi, op.fcg e ophandler.lua, -para FastCGI e Xavante respectivamente. Uma instalação Kepler padrão, -inclui suporte para Orbit Pages por default.

- -

Uma Orbit Page é também um template que utiliza a linguagem de template -Cosmo. O ambiente deste template é uma sandbox -que envolve o ambiente global e é recriado cada vez que é solicitado. -A principal variável deste ambiente é a variável web, que é um objeto request/response do Orbit.

- -

Outras variáveis importantes:

- -

mapper - uma instância default do Orbit ORM

- -

model(name, dao) - é o mesmo que mapper:new(name, dao), salvo quando name -é uma tabela, neste caso, chama mapper:new(name[1], name[2]), -logo você pode utilizar isso no template como $model{ name, dao }

- -

app - é o ambiente global da aplicação, que deve ser usado como um cache de sessão -(para conexões de dados, por exemplo) para disparadores persistentes

- -

finish(res) - suspende a execução da página atual, e envia res como resposta -no lugar do conteúdo da página

- -

redirect(target) - o mesmo que web:redirect(target) seguido por finish(). Se target -for uma tabela, equivale a web:redirect(target[1]), então, você pode usar isto no template -como $redirect{ target }

- -

include(page, [env]) - avalia o Orbit Page no arquivo page -(relativo ao path da página atual), com a opção de utilizar as variaveis em env -no ambiente do template. Também pode ser utilizado no template como $include{ page, env }

- -

forward(page, [env]) - interrompe a execução da pagina atual, -avalia e envia a página no arquivo page no lugar; de outra forma o mesmo que include

- -

Também existem outras poucas variaveis que deveriam ser usadas somente no template:

- -

$lua{ code } - roda code no mesmo ambiente que a página, -logo code pode mudar as variáveis do template e até mesmo definir novas

- -

$if{ condition }[[ then-part ]],[[ else-part ]] - se condition for verdade, -será subistituida pelo resultado da avaliação do template do then-part, -de outra forma else-part. else-part é opcional, sendo seu valor padrão definido como vazio

- - -

$fill{ ... }[[ template ]] - substituída pela avaliação do template utilizando -o ambiente passado para preencher (template não herda as variáveis da página)

- -

Abaixo vemos uma Orbit Page extremamente simples que mostra a maioria dos conceitos acima, -(incluindo o Cosmo, veja a documentação do Cosmo para maiores detalhes):

- - -
-#!/usr/bin/env op.cgi 
- 
- 
-

Hello Orbit!

-

I am in $web|real_path, and the script is -$web|script_name.

-$lua{[[ -if not web.input.msg then -web.input.msg = "nothing" -end -]]} -

You passed: $web|input|msg.

-$include{ "bar.op" } - - -
- - -

A página bar.op que ela inclui é essa:

- - -
-#!/usr/bin/env op.cgi 
-

This is bar, and you passed $web|input|msg!

-
- - -

A distribuição do Kepler tem exemplos mais completos que possuem -acesso ao banco de dados, POST, e até mesmo de alguns AJAX simples.

- - -
- -
- - - -
- - - - diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/doc/br/pages.md lua-orbit-2.2.1+dfsg/doc/br/pages.md --- lua-orbit-2.2.0+gita6fb46e+dfsg/doc/br/pages.md 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/doc/br/pages.md 1970-01-01 00:00:00.000000000 +0000 @@ -1,82 +0,0 @@ -## Orbit Pages - -Orbit Pages é um ambiente de programação similar ao PHP, construido sobre o Orbit. -As Orbit Pages são páginas HTML (mas que utilizam a extensão .op em uma instalação do Orbit típica) -que são convertidas dinamicamente em uma aplicação Orbit. -Elas são inicializadas pelos disparadores op.cgi, op.fcg e ophandler.lua, -para FastCGI e Xavante respectivamente. Uma instalação Kepler padrão, -inclui suporte para Orbit Pages por default. - -Uma Orbit Page é também um template que utiliza a linguagem de template -[Cosmo](http://cosmo.luaforge.net). O ambiente deste template é uma *sandbox* -que envolve o ambiente global e é recriado cada vez que é solicitado. -A principal variável deste ambiente é a variável `web`, que é um objeto request/response do Orbit. - -Outras variáveis importantes: - -**mapper** - uma instância default do Orbit ORM - -**model(*name*, *dao*)** - é o mesmo que mapper:new(*name*, *dao*), salvo quando *name* -é uma tabela, neste caso, chama mapper:new(*name[1]*, *name[2]*), -logo você pode utilizar isso no template como `$model{ name, dao }` - -**app** - é o ambiente global da aplicação, que deve ser usado como um cache de sessão -(para conexões de dados, por exemplo) para disparadores persistentes - -**finish(*res*)** - suspende a execução da página atual, e envia *res* como resposta -**no lugar** do conteúdo da página - -**redirect(*target*)** - o mesmo que web:redirect(*target*) seguido por finish(). Se *target* -for uma tabela, equivale a web:redirect(*target[1]*), então, você pode usar isto no template -como `$redirect{ target }` - -**include(*page*, [*env*])** - avalia o Orbit Page no arquivo *page* -(relativo ao path da página atual), com a opção de utilizar as variaveis em *env* -no ambiente do template. Também pode ser utilizado no template como `$include{ page, env }` - -**forward(*page*, [*env*])** - interrompe a execução da pagina atual, -avalia e envia a página no arquivo *page* no lugar; de outra forma o mesmo que **include** - -Também existem outras poucas variaveis que deveriam ser usadas somente no template: - -**$lua{ *code* }** - roda *code* no mesmo ambiente que a página, -logo *code* pode mudar as variáveis do template e até mesmo definir novas - -**$if{ *condition* }[[ *then-part* ]],[[ *else-part* ]]** - se *condition* for verdade, -será subistituida pelo resultado da avaliação do template do *then-part*, -de outra forma *else-part*. *else-part* é opcional, sendo seu valor padrão definido como vazio - - -**$fill{ ... }[[ *template* ]]** - substituída pela avaliação do *template* utilizando -o ambiente passado para preencher (*template* **não** herda as variáveis da página) - -Abaixo vemos uma Orbit Page extremamente simples que mostra a maioria dos conceitos acima, -(incluindo o Cosmo, veja a documentação do Cosmo para maiores detalhes): - -
-#!/usr/bin/env op.cgi 
- 
- 
-

Hello Orbit!

-

I am in $web|real_path, and the script is -$web|script_name.

-$lua{[[ -if not web.input.msg then -web.input.msg = "nothing" -end -]]} -

You passed: $web|input|msg.

-$include{ "bar.op" } - - -
- -A página `bar.op` que ela inclui é essa: - -
-#!/usr/bin/env op.cgi 
-

This is bar, and you passed $web|input|msg!

-
- -A distribuição do [Kepler](http://www.keplerproject.org) tem exemplos mais completos que possuem -acesso ao banco de dados, POST, e até mesmo de alguns AJAX simples. \ No newline at end of file diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/doc/br/reference.html lua-orbit-2.2.1+dfsg/doc/br/reference.html --- lua-orbit-2.2.0+gita6fb46e+dfsg/doc/br/reference.html 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/doc/br/reference.html 1970-01-01 00:00:00.000000000 +0000 @@ -1,260 +0,0 @@ - - - - - Orbit - - - - - - -
- -
- - -
Orbit
-
MVC desenvolvimento Web em Lua
-
- -
- - - -
- - -

Manual de Referência

- -

Este é um pequeno manual de refência dos métodos do Orbit e sua aplicações.

- -

Módulo orbit

- -

orbit.new([app]) - cria uma nova aplicação Orbit, retornando esta (como um módulo). -Se app for uma string este é o nome da aplicação e define o campo_NAME. -Se app for uma tabela, esta será usada no lugar de uma tabela vazia. -Isso quer dizer que você pode passar orbit.new para a função module

- -

orbit.htmlify(func[, func...]) - modifica o ambiente de func para incluir funções que gerem HTML

- -

Aplicações Orbit

- -

app.run(wsapi_env) - ponto de entrada WSAPI para aplicações, gerado pelo Orbit

- -

app.real_path - A raiz da aplicação no sistema de arquivos, -por default é o path inferido pelo disparador WSAPI (wsapi.app\_path), mas pode ser redefinido

- -

app.mapper - mapeador usado pelo app:model, por default é uma instância do orbit.model, -mas pode ser invalidado

- -

app.not_found - tratador padrão, envia uma respota 404 para o cliente, pode ser redefinido. -O handler recebe um objeto web

- -

app.server_error - tratador de erro, envia uma resposta 500 com os detalhes de stack para o cliente, -pode ser redefinido. O tratador recebe um objeto web

- -

app:dispatch_get(func, patt[, patt...]) - instala a função func* como um tratador de GET -para os padrões patt. func recebe um objeto web e capturas

- -

app:dispatch_post(func, patt[, patt...]) - instala func como um tratador de POST -para os padrões patt. func recebe um objeto web e capturas

- -

app:dispatch_wsapi(func, patt[, patt...]) - instala func como um tratador WSAPI -para os padrões patt. func recebe um objeto web e capturas

- -

app:dispatch_static(patt[, patt...]) - instala um tratador de arquivos estáticos -para os padrões patt. Este tratador assume que PATH_INFO é um arquivo relativo a -app.real_path e o envia para o cliente. O tipo MIME é detectado pela extensão -(sendo o padrão application/octec-stream).

- -

app:serve_static(web, filename) - retorna o conteúdo do arquivo filename -(que pode estar em qualquer lugar do sistema), e define os cabeçalhos apropriados -de acordo com o tipo MIME do arquivo

- -

app:htmlify(patt[, patt...]) - o mesmo que orbit.htmlify, -mas altera todas funções do módulo da aplicação que correspondem aos padrões patt

- -

app:model(...) - chama app.mapper:new(...), -de forma que o comportamento depende do mapeador que você estiver usando

- -

Métodos `web'

- -

Os objetos web herdam as funções do módulo wsapi.util como métodos

- -

web.status - status para ser enviado para o servidor (padrão: "200 Ok")

- -

web.headers - cabeçalhos para serem enviados para o servidor, -uma tabela Lua (que por padrão tem Content-Type como text/html)

- -

web.response - corpo a ser enviado para o cliente (a princípio vazio)

- -

web.vars - ambiente WSAPI original

- -

web.prefix - prefixo da aplicação (se determinado no módulo da aplicação) ou SCRIPT_NAME

- -

web.suffix - sufixo da aplicação (se determinado no módulo da aplicação)

- -

web.real_path - localização da aplicação no sistema, obtido a partir de wsapi_env.APP_PATH, -ou do real_path do módulo, ou "."

- -

web.doc_root, web.path_info, web.script_name, web.path_translated, web.method - -raiz de documentos do servidor, PATH_INFO, SCRIPT_NAME, PATH_TRANSLATED, -e REQUEST_METHOD (convertido para minúsculas)

- -

web.GET, web.POST - variáveis GET and POST

- -

web.input - união de web.GET e web.POST

- -

web.cookies - cookies enviados pelo browser

- -

web:set_cookie(name, value) - define um cookie a ser enviado de volta ao browser

- -

web:delete_cookie(name) - apaga um cookie

- -

web:redirect(url) - define o status e cabeçalhos e redireciona o cliente para url

- -

web:link(part, [params]) - cria um link interno da aplicação, -utilizando web.prefix e web.suffix, e codificando params como uma query string

- -

web:static_link(part) - se o ponto de entrada de uma aplicação é um script, -ao invés de um path, cria um link para o vpath da aplicação -(por exemplo, se o app.prefix for /foo/app.ws, cria um link para /foo/part), -caso contrário equivale a web:link

- -

web:empty(s) - retorna true se s for nil ou uma string vazia (com zero ou mais espaços)

- -

web:empty_param(name) - retorna true se os parâmetros de entrada forem vazios (como web:empty)

- -

web:page(name, [env]) - carrega e trata uma página Orbit de nome name. -Se name se inicia com / a página é relativa à raiz de documentos, -caso contrário é relativa ao path da aplicação. Retorna a página tratada. -env é um ambiente opcional com variáveis extra

- -

web:page_inline(contents, [env]) - trata uma página Orbit embutida

- -

Módulo orbit.cache

- -

orbit.cache.new(app, [base_path]) - cria um cache de páginas para a aplicação app, -em memória ou no sistema de arquivos como base_path (não relativo ao path da aplicação!), -retorna o objeto de cache

- -

a_cache(handler) - torna o tratador handler cacheado, retornando um novo tratador; -utiliza o PATH_INFO como chave do cache

- -

a_cache:get(key) - obtém o valor armazenado na chave key; -geralmente não utilizada, use a função anterior

- -

a_cache:set(key, val) - armazena um valor no cache; -use a_cache(handler) para encapsular este comportamento

- -

a_cache:invalidate(key) - invalida um valor do cache

- -

a_cache:nuke() - esvazia o cache

- -

Módulo orbit.model

- -

orbit.model.new([table_prefix], [conn], [driver]) - cria um novo mapeamento ORM (objeto relacional). -table_prefix (padrão "") é uma string adicionada ao início dos nomes de modelos para obter nomes de tabelas; -conn é a conexão de banco de dados (pode ser definida posteriormente); -driver é o tipo de banco de dados (atualmente "sqlite3", o padrão, ou "mysql"). -Retorna uma instância do mapeador e todos os parâmetros podem ser definidos -após a criação desta instância (utilizando a_mapper.table_prefix, a_mapper.conn e a_mapper.driver)

- -

a_mapper:new(name, [tab]) - cria um novo objeto de modelo; -name é usado junto com a_mapper.table_prefix para formar o nome da tabela no banco de dados; -campos e tipos são instrospectados a partir da tabela. -tab é uma tabela opcional que é usada como base para o objeto modelo, caso exista

- -

a_model.model - o mapeador para este modelo

- -

a_model.name, a_model.table_name - o nome do modelo e de sua tabela de armazenamento

- -

a_model.driver - o driver de banco de dados utilizado

- -

a_model.meta - metainformação sobre o modelo, instrospectada a partir da tabela de dados

- -

a_model:find(id) - busca e retorna a instância do modelo com o id passado -(indexado através da coluna id, numérica,  da tabela)

- -

a_model:find_first(condition, args) - busca e retorna a primeira instância do modelo -que corresponde à condição condition; args pode determinar a ordem (args.order) ou injetar -campos de outras tabelas (args.inject)

- -

Exemplo: books:find_first("author = ? and year_pub > ?", { "John Doe", 1995, order = "year_pub asc" })

- -

a_model:find_all(condition, args) - busca e retorna todas as instâncias do modelo que correspondem -à condição condition; args pode determinar a ordem (args.order) ou injetar campos de outras tabelas -(args.inject)

- -

a_model:new([tab]) - cria uma nova instância do modelo, opcionalmente usando tab -como os valores iniciais

- -

a_model:find_by_xxx(args) - busca e retorna a primeira instância do modelo montando -a condição a partir do nome do método, como no ActiveRecord do Rails

- -

a_model:find_all_by_xxx(args) - busca e retorna todas as instâncias do modelo montando -a condição a partir do nome do método, como no ActiveRecord do Rails

- -

Exemplo: books:find_all_by_author_or_author{ "John Doe", "Jane Doe", order = "year_pub asc" }

- -

an_instance:save([force_insert]) - grava uma instância no banco de dados, -atualizando as mudanças ou criando um novo registro caso a instância seja nova; -se force_insert for true sempre faz uma inserção antes de uma atualização

- -

Se existir uma coluna chamada created_at esta linha é definida como a data de criação do registro; - se existir uma coluna chamada updated_at, esta linha é definida como a data de última atualização do registro.

- -

an_instance:delete() - remove uma instância do banco de dados

- - - - -
- -
- - - -
- - - - diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/doc/br/reference.md lua-orbit-2.2.1+dfsg/doc/br/reference.md --- lua-orbit-2.2.0+gita6fb46e+dfsg/doc/br/reference.md 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/doc/br/reference.md 1970-01-01 00:00:00.000000000 +0000 @@ -1,182 +0,0 @@ -## Manual de Referência - -Este é um pequeno manual de refência dos métodos do Orbit e sua aplicações. - -## Módulo `orbit` - -**orbit.new([*app*])** - cria uma nova aplicação Orbit, retornando esta (como um módulo). -Se *app* for uma string este é o nome da aplicação e define o campo\_NAME. -Se *app* for uma tabela, esta será usada no lugar de uma tabela vazia. -Isso quer dizer que você pode passar `orbit.new` para a função `module` - -**orbit.htmlify(*func*[, *func*...])** - modifica o ambiente de *func* para incluir funções que gerem HTML - -## Aplicações Orbit - -**app.run(*wsapi\_env*)** - ponto de entrada WSAPI para aplicações, gerado pelo Orbit - -**app.real\_path** - A raiz da aplicação no sistema de arquivos, -por default é o path inferido pelo disparador WSAPI (`wsapi.app\_path`), mas pode ser redefinido - -**app.mapper** - mapeador usado pelo `app:model`, por default é uma instância do `orbit.model`, -mas pode ser invalidado - -**app.not\_found** - tratador padrão, envia uma respota 404 para o cliente, pode ser redefinido. -O handler recebe um objeto `web` - -**app.server\_error** - tratador de erro, envia uma resposta 500 com os detalhes de stack para o cliente, -pode ser redefinido. O tratador recebe um objeto `web` - -**app:dispatch\_get(*func*, *patt*[, *patt*...])** - instala a função func* como um tratador de GET -para os padrões *patt*. *func* recebe um objeto `web` e capturas - -**app:dispatch\_post(*func*, *patt*[, *patt*...])** - instala *func* como um tratador de POST -para os padrões *patt*. *func* recebe um objeto `web` e capturas - -**app:dispatch\_wsapi(*func*, *patt*[, *patt*...])** - instala *func* como um tratador WSAPI -para os padrões *patt*. *func* recebe um objeto `web` e capturas - -**app:dispatch\_static(*patt*[, *patt*...])** - instala um tratador de arquivos estáticos -para os padrões *patt*. Este tratador assume que PATH\_INFO é um arquivo relativo a -`app.real_path` e o envia para o cliente. O tipo MIME é detectado pela extensão -(sendo o padrão application/octec-stream). - -**app:serve\_static(*web*, *filename*)** - retorna o conteúdo do arquivo *filename* -(que pode estar em qualquer lugar do sistema), e define os cabeçalhos apropriados -de acordo com o tipo MIME do arquivo - -**app:htmlify(*patt*[, *patt*...])** - o mesmo que `orbit.htmlify`, -mas altera todas funções do módulo da aplicação que correspondem aos padrões *patt* - -**app:model(...)** - chama `app.mapper:new(...)`, -de forma que o comportamento depende do mapeador que você estiver usando - -## Métodos `web' - -Os objetos *web* herdam as funções do módulo `wsapi.util` como métodos - -**web.status** - status para ser enviado para o servidor (padrão: "200 Ok") - -**web.headers** - cabeçalhos para serem enviados para o servidor, -uma tabela Lua (que por padrão tem Content-Type como text/html) - -**web.response** - corpo a ser enviado para o cliente (a princípio vazio) - -**web.vars** - ambiente WSAPI original - -**web.prefix** - prefixo da aplicação (se determinado no módulo da aplicação) ou SCRIPT\_NAME - -**web.suffix** - sufixo da aplicação (se determinado no módulo da aplicação) - -**web.real\_path** - localização da aplicação no sistema, obtido a partir de wsapi\_env.APP\_PATH, -ou do real\_path do módulo, ou "." - -**web.doc\_root, web.path\_info, web.script\_name, web.path\_translated, web.method** - -raiz de documentos do servidor, PATH\_INFO, SCRIPT\_NAME, PATH\_TRANSLATED, -e REQUEST\_METHOD (convertido para minúsculas) - -**web.GET, web.POST** - variáveis GET and POST - -**web.input** - união de web.GET e web.POST - -**web.cookies** - cookies enviados pelo browser - -**web:set\_cookie(*name*, *value*)** - define um cookie a ser enviado de volta ao browser - -**web:delete\_cookie(*name*)** - apaga um cookie - -**web:redirect(*url*)** - define o status e cabeçalhos e redireciona o cliente para *url* - -**web:link(*part*, [*params*])** - cria um link interno da aplicação, -utilizando web.prefix e web.suffix, e codificando *params* como uma query string - -**web:static\_link(*part*)** - se o ponto de entrada de uma aplicação é um script, -ao invés de um path, cria um link para o vpath da aplicação -(por exemplo, se o app.prefix for /foo/app.ws, cria um link para /foo/*part*), -caso contrário equivale a web:link - -**web:empty(*s*)** - retorna true se *s* for nil ou uma string vazia (com zero ou mais espaços) - -**web:empty\_param(*name*)** - retorna true se os parâmetros de entrada forem vazios (como web:empty) - -**web:page(*name*, [*env*])** - carrega e trata uma página Orbit de nome *name*. -Se *name* se inicia com / a página é relativa à raiz de documentos, -caso contrário é relativa ao path da aplicação. Retorna a página tratada. -*env* é um ambiente opcional com variáveis extra - -**web:page_inline(*contents*, [*env*])** - trata uma página Orbit embutida - -## Módulo `orbit.cache` - -**orbit.cache.new(*app*, [*base\_path*])** - cria um cache de páginas para a aplicação *app*, -em memória ou no sistema de arquivos como *base\_path* (*não* relativo ao path da aplicação!), -retorna o objeto de cache - -**a\_cache(*handler*)** - torna o tratador *handler* cacheado, retornando um novo tratador; -utiliza o PATH\_INFO como chave do cache - -**a\_cache:get(*key*)** - obtém o valor armazenado na chave *key*; -geralmente não utilizada, use a função anterior - -**a\_cache:set(*key*, *val*)** - armazena um valor no cache; -use a\_cache(*handler*) para encapsular este comportamento - -**a\_cache:invalidate(*key*)** - invalida um valor do cache - -**a\_cache:nuke()** - esvazia o cache - -## Módulo `orbit.model` - -**orbit.model.new([*table\_prefix*], [*conn*], [*driver*])** - cria um novo mapeamento ORM (objeto relacional). -*table\_prefix* (padrão "") é uma string adicionada ao início dos nomes de modelos para obter nomes de tabelas; -*conn* é a conexão de banco de dados (pode ser definida posteriormente); -*driver* é o tipo de banco de dados (atualmente "sqlite3", o padrão, ou "mysql"). -Retorna uma instância do mapeador e todos os parâmetros podem ser definidos -após a criação desta instância (utilizando a\_mapper.table\_prefix, a\_mapper.conn e a\_mapper.driver) - -**a\_mapper:new(*name*, [*tab*])** - cria um novo objeto de modelo; -*name* é usado junto com a\_mapper.table\_prefix para formar o nome da tabela no banco de dados; -campos e tipos são instrospectados a partir da tabela. -*tab* é uma tabela opcional que é usada como base para o objeto modelo, caso exista - -**a\_model.model** - o mapeador para este modelo - -**a\_model.name, a\_model.table\_name** - o nome do modelo e de sua tabela de armazenamento - -**a\_model.driver** - o driver de banco de dados utilizado - -**a\_model.meta** - metainformação sobre o modelo, instrospectada a partir da tabela de dados - -**a\_model:find(*id*)** - busca e retorna a instância do modelo com o *id* passado -(indexado através da coluna `id`, numérica,  da tabela) - -**a\_model:find\_first(*condition*, *args*)** - busca e retorna a primeira instância do modelo -que corresponde à condição *condition*; *args* pode determinar a ordem (args.order) ou injetar -campos de outras tabelas (args.inject) - -Exemplo: `books:find_first("author = ? and year_pub > ?", { "John Doe", 1995, order = "year_pub asc" })` - -**a\_model:find\_all(*condition*, *args*)** - busca e retorna todas as instâncias do modelo que correspondem -à condição *condition*; *args* pode determinar a ordem (args.order) ou injetar campos de outras tabelas -(args.inject) - -**a\_model:new([*tab*])** - cria uma nova instância do modelo, opcionalmente usando *tab* -como os valores iniciais - -**a\_model:find\_by\_xxx(*args*)** - busca e retorna a primeira instância do modelo montando -a condição a partir do nome do método, como no ActiveRecord do Rails - -**a\_model:find\_all\_by\_xxx(*args*)** - busca e retorna todas as instâncias do modelo montando -a condição a partir do nome do método, como no ActiveRecord do Rails - -Exemplo: `books:find_all_by_author_or_author{ "John Doe", "Jane Doe", order = "year_pub asc" }` - -**an\_instance:save([*force\_insert*])** - grava uma instância no banco de dados, -atualizando as mudanças ou criando um novo registro caso a instância seja nova; -se *force\_insert* for true sempre faz uma inserção antes de uma atualização - -Se existir uma coluna chamada `created_at` esta linha é definida como a data de criação do registro; - se existir uma coluna chamada `updated_at`, esta linha é definida como a data de última atualização do registro. - -**an\_instance:delete()** - remove uma instância do banco de dados - diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/doc/us/blog_config.lua lua-orbit-2.2.1+dfsg/doc/us/blog_config.lua --- lua-orbit-2.2.0+gita6fb46e+dfsg/doc/us/blog_config.lua 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/doc/us/blog_config.lua 1970-01-01 00:00:00.000000000 +0000 @@ -1,160 +0,0 @@ - --- Puts all the definitions below in blog's namespace -module("blog", package.seeall) - -blog_title = "Blog" - -cache_path = "page_cache" - -copyright_notice = "Copyright 2007 Foobar" - -about_blurb = [[This is an example of a blog built using Orbit. You -can browse posts and add comments, but to add new posts you have -to go directly to the database. This will be fixed in the future.]] - -blogroll = { - { "http://slashdot.org", "Slashdot"}, - { "http://news.google.com", "Google News" }, - { "http://www.wikipedia.org", "Wikipedia" }, -} - --- Uncomment this to send static files through X-Sendfile --- use_xsendfile = true - -database = { --- driver = "mysql", --- conn_data = { "blog", "root", "password" } - driver = "sqlite3", - conn_data = { blog.real_path .. "/blog.db" } -} - -recent_count = 7 - -strings = {} - -strings.pt = { - home_page_name = "Página Inicial", - about_title = "Sobre o Blog", - last_posts = "Últimos Posts", - blogroll_title = "Links", - archive_title = "Arquivo", - anonymous_author = "Anônimo", - no_posts = "No há posts publicados.", - published_at = "Publicado às", - comments = "Comentários", - written_by = "Escrito por", - on_date = "em", - new_comment = "Novo comentário", - no_comment = "Você esqueceu o comentário!", - form_name = "Nome:", - form_email = "Email:", - form_url = "Site:", - italics = "itálico", - bold = "negrito", - link = "link", - send = "Enviar" -} - -strings.en = { - home_page_name = "Home Page", - about_title = "About this Blog", - last_posts = "Recent Posts", - blogroll_title = "Links", - archive_title = "Archives", - anonymous_author = "Anonymous", - no_posts = "No published posts.", - published_at = "Published at", - comments = "Comments", - written_by = "Written by", - on_date = "at", - new_comment = "New comment", - no_comment = "You forgot the comment!", - form_name = "Name:", - form_email = "Email:", - form_url = "Site:", - italics = "italics", - bold = "bold", - link = "link", - send = "Send" -} - -language = "en" - -strings = strings[language] - -months = {} - -months.pt = { "Janeiro", "Fevereiro", "Março", "Abril", - "Maio", "Junho", "Julho", "Agosto", "Setembro", "Outubro", - "Novembro", "Dezembro" } - -months.en = { "January", "February", "March", "April", - "May", "June", "July", "August", "September", "October", - "November", "December" } - -weekdays = {} - -weekdays.pt = { "Domingo", "Segunda", "Terça", "Quarta", - "Quinta", "Sexta", "Sábado" } - -weekdays.en = { "Sunday", "Monday", "Tuesday", "Wednesday", - "Thursday", "Friday", "Saturday" } - --- Utility functions - -time = {} -date = {} -month = {} - -local datetime_mt = { __call = function (tab, date) return tab[language](date) end } - -setmetatable(time, datetime_mt) -setmetatable(date, datetime_mt) -setmetatable(month, datetime_mt) - -function time.pt(date) - local time = os.date("%H:%M", date) - date = os.date("*t", date) - return date.day .. " de " - .. months.pt[date.month] .. " de " .. date.year .. " às " .. time -end - -function date.pt(date) - date = os.date("*t", date) - return weekdays.pt[date.wday] .. ", " .. date.day .. " de " - .. months.pt[date.month] .. " de " .. date.year -end - -function month.pt(month) - return months.pt[month.month] .. " de " .. month.year -end - -local function ordinalize(number) - if number == 1 then - return "1st" - elseif number == 2 then - return "2nd" - elseif number == 3 then - return "3rd" - else - return tostring(number) .. "th" - end -end - -function time.en(date) - local time = os.date("%H:%M", date) - date = os.date("*t", date) - return months.en[date.month] .. " " .. ordinalize(date.day) .. " " .. - date.year .. " at " .. time -end - -function date.en(date) - date = os.date("*t", date) - return weekdays.en[date.wday] .. ", " .. months.en[date.month] .. " " .. - ordinalize(date.day) .. " " .. date.year -end - -function month.en(month) - return months.en[month.month] .. " " .. month.year -end - diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/doc/us/example.html lua-orbit-2.2.1+dfsg/doc/us/example.html --- lua-orbit-2.2.0+gita6fb46e+dfsg/doc/us/example.html 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/doc/us/example.html 2014-02-02 09:25:01.000000000 +0000 @@ -88,26 +88,31 @@
-require "orbit"
-require "orbit.cache"
-require "markdown"
+local orbit = require "orbit"
+local orcache = require "orbit.cache"
+local markdown = require "markdown"
+local wsutil = require "wsapi.util"
 

In this example we are going to use Orbit's page cache, and the Markdown parser for marking up posts.

-

All Orbit applications are Lua modules, so we include this line:

+

We will now create the blog application and set it as the global environment for the +rest of the module:

-module("blog", package.seeall, orbit.app)
+local blog = setmetatable(orbit.new(), { __index = _G })
+if _VERSION == "Lua 5.2" then
+  _ENV = blog
+else
+  setfenv(1, blog)
+end
 
-

This sets up the blog module and initializes it as an Orbit application.

- -

orbit.app injects quite a lot of stuff in the blog module's namespace. +

orbit.new injects quite a lot of stuff in the blog module's namespace. The most important of these are the dispatch_get, dispatch_post, and model methods that let you define the main functionality of the application. It also defines a mapper variable that Orbit uses to create @@ -121,7 +126,7 @@

-require "blog_config"
+wsutil.loadfile("blog_config.lua", blog)()
 
@@ -130,7 +135,7 @@
-require("luasql." .. database.driver)
+local luasql = require("luasql." .. database.driver)
 local env = luasql[database.driver]()
 mapper.conn = env:connect(unpack(database.conn_data))
 mapper.driver = database.driver
@@ -791,6 +796,15 @@
 HTML generation. Here we set the layout function, all the render_ functions,
 and all the helpers (the functions starting with _).

+

We end the file by returning the module:

+ + +
+return blog
+
+ + +

Deployment

For this part of the tutorial it is better if you go to the samples/blog folder @@ -805,8 +819,7 @@

 #!/usr/bin/env wsapi.cgi
-require "blog"
-return blog
+return require "blog"
 
diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/doc/us/example.md lua-orbit-2.2.1+dfsg/doc/us/example.md --- lua-orbit-2.2.0+gita6fb46e+dfsg/doc/us/example.md 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/doc/us/example.md 2014-02-02 09:25:01.000000000 +0000 @@ -20,23 +20,28 @@ are going to use in your app:
-require "orbit"
-require "orbit.cache"
-require "markdown"
+local orbit = require "orbit"
+local orcache = require "orbit.cache"
+local markdown = require "markdown"
+local wsutil = require "wsapi.util"
 
In this example we are going to use Orbit's page cache, and the Markdown parser for marking up posts. -All Orbit applications are Lua modules, so we include this line: +We will now create the `blog` application and set it as the global environment for the +rest of the module:
-module("blog", package.seeall, orbit.app)
+local blog = setmetatable(orbit.new(), { __index = _G })
+if _VERSION == "Lua 5.2" then
+  _ENV = blog
+else
+  setfenv(1, blog)
+end
 
-This sets up the `blog` module and initializes it as an Orbit application. - -`orbit.app` injects quite a lot of stuff in the `blog` module's namespace. +`orbit.new` injects quite a lot of stuff in the `blog` module's namespace. The most important of these are the `dispatch_get`, `dispatch_post`, and `model` methods that let you define the main functionality of the application. It also defines a `mapper` variable that Orbit uses to create @@ -49,14 +54,14 @@ You can get this script from [here](blog_config.lua).
-require "blog_config"
+wsutil.loadfile("blog_config.lua", blog)()
 
The next few lines load one of LuaSQL's database driver (defined in the configuration), and sets up Orbit's OR mapper.
-require("luasql." .. database.driver)
+local luasql = require("luasql." .. database.driver)
 local env = luasql[database.driver]()
 mapper.conn = env:connect(unpack(database.conn_data))
 mapper.driver = database.driver
@@ -668,6 +673,13 @@
 HTML generation. Here we set the `layout` function, all the `render_` functions,
 and all the helpers (the functions starting with `_`).
 
+We end the file by returning the module:
+
+
+return blog
+
+ + ## Deployment For this part of the tutorial it is better if you go to the `samples/blog` folder @@ -681,8 +693,7 @@
 #!/usr/bin/env wsapi.cgi
-require "blog"
-return blog
+return require "blog"
 
Depending on your configuration, you might need to install the `luasql-sqlite3` and diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/doc/us/index.html lua-orbit-2.2.1+dfsg/doc/us/index.html --- lua-orbit-2.2.0+gita6fb46e+dfsg/doc/us/index.html 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/doc/us/index.html 2014-02-02 09:25:01.000000000 +0000 @@ -68,7 +68,7 @@

Overview

Orbit is an MVC web framework for Lua. The design is inspired by lightweight Ruby -frameworks such as Camping. It completely +frameworks such as Camping. It completely abandons the CGILua model of "scripts" in favor of applications, where each Orbit application can fit in a single file, but you can split it into multiple files if you want. All Orbit applications follow the WSAPI protocol, so they currently @@ -78,6 +78,15 @@

History

    +
  • Version 2.2.1 (12/Jan/2014)

    +
      +
    • bugfix release for Lua 5.1
    • +
    • NOT 5.2 compliant
    • +
    • documentation corrections and updates
    • +
    • support for Wsapi 1.6 and other dependency modules that no longer use "module"
    • +
    • additional orbit model datatypes: real, float, timestamp, numeric
    • +
    • MIME type application/json included
    • +
  • Version 2.2.0 (31/Mar/2010)

    • Reparse response to resume the dispatcher
    • @@ -94,7 +103,7 @@
    • new options for orbit.model finders: distinct, fields
    • count option for orbit.model now limits number of rows in the SQL
    • logging of queries in orbit.model
    • -
    • overhaul of the "orbit" script: better options, --help, sets application paht
    • +
    • overhaul of the "orbit" script: better options, --help, sets application path
    • content_type method in the web object to set content type
    • support for PUT and DELETE (methods dispatch_put and dispatch_delete)
    • orbit.model.recycle(*conn_builder, *timeout) function, to make a connection that @@ -116,7 +125,7 @@
    • fixed bugs 13451 and 25418: setting status 500 on application errors not throwing an error if file not exists when invalidating cache
  • Version 2.0.1 (10/Jun/2008): bug-fix release, fixed bug in Orbit pages' redirect function (thanks for - Ignacio Burgueno for finding the bug)

  • + Ignacio Burgueño for finding the bug)

  • Version 2.0 (06/Jun/2008): Complete rewrite of Orbit

  • Version 1.0: Initial release, obsolete

@@ -140,7 +149,7 @@
     #!/usr/bin/env wsapi.cgi
 
-    require"orbit"
+    local orbit = require "orbit"
 
     -- Orbit applications are usually modules,
     -- orbit.new does the necessary initialization
@@ -255,7 +264,7 @@
 Comments are welcome!

You can also reach us and other developers and users on the Kepler Project -mailing list.

+mailing list.

diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/doc/us/index.md lua-orbit-2.2.1+dfsg/doc/us/index.md --- lua-orbit-2.2.0+gita6fb46e+dfsg/doc/us/index.md 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/doc/us/index.md 2014-02-02 09:25:01.000000000 +0000 @@ -1,7 +1,7 @@ ## Overview Orbit is an MVC web framework for Lua. The design is inspired by lightweight Ruby -frameworks such as [Camping](http://code.whytheluckystiff.net/camping/). It completely +frameworks such as [Camping](http://en.wikipedia.org/wiki/Camping_%28microframework%29). It completely abandons the CGILua model of "scripts" in favor of applications, where each Orbit application can fit in a single file, but you can split it into multiple files if you want. All Orbit applications follow the [WSAPI](http://keplerproject.github.com/wsapi) protocol, so they currently @@ -10,6 +10,14 @@ ## History +* Version 2.2.1 (12/Jan/2014) + * bugfix release for Lua 5.1 + * NOT 5.2 compliant + * documentation corrections and updates + * support for Wsapi 1.6 and other dependency modules that no longer use "module" + * additional orbit model datatypes: real, float, timestamp, numeric + * MIME type application/json included + * Version 2.2.0 (31/Mar/2010) * Reparse response to resume the dispatcher * better parser for orbit.model conditions, fixes parsing bugs @@ -24,7 +32,7 @@ * new options for orbit.model finders: distinct, fields * count option for orbit.model now limits number of rows in the SQL * logging of queries in orbit.model - * overhaul of the "orbit" script: better options, --help, sets application paht + * overhaul of the "orbit" script: better options, --help, sets application path * content_type method in the web object to set content type * support for PUT and DELETE (methods `dispatch_put` and `dispatch_delete`) * orbit.model.recycle(*conn_builder*, *timeout*) function, to make a connection that @@ -45,7 +53,7 @@ * fixed bugs 13451 and 25418: setting status 500 on application errors not throwing an error if file not exists when invalidating cache * Version 2.0.1 (10/Jun/2008): bug-fix release, fixed bug in Orbit pages' redirect function (thanks for -Ignacio Burgueno for finding the bug) +Ignacio Burgueño for finding the bug) * Version 2.0 (06/Jun/2008): Complete rewrite of Orbit @@ -69,7 +77,7 @@
     #!/usr/bin/env wsapi.cgi
 
-    require"orbit"
+    local orbit = require "orbit"
 
     -- Orbit applications are usually modules,
     -- orbit.new does the necessary initialization
@@ -181,4 +189,4 @@
 Comments are welcome!
 
 You can also reach us and other developers and users on the Kepler Project 
-[mailing list](http://luaforge.net/mail/?group_id=104). 
+[mailing list](https://groups.google.com/forum/#!forum/kepler-project). 
diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/doc/us/makedoc.lua lua-orbit-2.2.1+dfsg/doc/us/makedoc.lua
--- lua-orbit-2.2.0+gita6fb46e+dfsg/doc/us/makedoc.lua	2013-08-13 08:16:46.000000000 +0000
+++ lua-orbit-2.2.1+dfsg/doc/us/makedoc.lua	2014-02-02 09:25:01.000000000 +0000
@@ -1,5 +1,5 @@
 local cosmo = require "cosmo"
-require "markdown"
+local markdown = require "markdown"
 
 local pages = {
   { name = "Home", file = "index", sections = {} },
diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/doc/us/markdown.lua lua-orbit-2.2.1+dfsg/doc/us/markdown.lua
--- lua-orbit-2.2.0+gita6fb46e+dfsg/doc/us/markdown.lua	1970-01-01 00:00:00.000000000 +0000
+++ lua-orbit-2.2.1+dfsg/doc/us/markdown.lua	2014-02-02 09:25:01.000000000 +0000
@@ -0,0 +1,1365 @@
+#!/usr/bin/env lua
+
+--[[
+# markdown.lua -- version 0.32
+
+
+
+**Author:** Niklas Frykholm, 
+**Date:** 31 May 2008
+
+This is an implementation of the popular text markup language Markdown in pure Lua.
+Markdown can convert documents written in a simple and easy to read text format
+to well-formatted HTML. For a more thourough description of Markdown and the Markdown
+syntax, see .
+
+The original Markdown source is written in Perl and makes heavy use of advanced
+regular expression techniques (such as negative look-ahead, etc) which are not available
+in Lua's simple regex engine. Therefore this Lua port has been rewritten from the ground
+up. It is probably not completely bug free. If you notice any bugs, please report them to
+me. A unit test that exposes the error is helpful.
+
+## Usage
+
+    require "markdown"
+    markdown(source)
+
+``markdown.lua`` exposes a single global function named ``markdown(s)`` which applies the
+Markdown transformation to the specified string.
+
+``markdown.lua`` can also be used directly from the command line:
+
+	lua markdown.lua test.md
+
+Creates a file ``test.html`` with the converted content of ``test.md``. Run:
+
+    lua markdown.lua -h
+
+For a description of the command-line options.
+
+``markdown.lua`` uses the same license as Lua, the MIT license.
+
+## License
+
+Copyright © 2008 Niklas Frykholm.
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this
+software and associated documentation files (the "Software"), to deal in the Software
+without restriction, including without limitation the rights to use, copy, modify, merge,
+publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
+to whom the Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all copies
+or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+THE SOFTWARE.
+
+## Version history
+
+-	**0.32** -- 31 May 2008
+	- Fix for links containing brackets
+-	**0.31** -- 1 Mar 2008
+	-	Fix for link definitions followed by spaces
+-	**0.30** -- 25 Feb 2008
+	-	Consistent behavior with Markdown when the same link reference is reused
+-	**0.29** -- 24 Feb 2008
+	-	Fix for 
 blocks with spaces in them
+-	**0.28** -- 18 Feb 2008
+	-	Fix for link encoding
+-	**0.27** -- 14 Feb 2008
+	-	Fix for link database links with ()
+-	**0.26** -- 06 Feb 2008
+	-	Fix for nested italic and bold markers
+-	**0.25** -- 24 Jan 2008
+	-	Fix for encoding of naked <
+-	**0.24** -- 21 Jan 2008
+	-	Fix for link behavior.
+-	**0.23** -- 10 Jan 2008
+	-	Fix for a regression bug in longer expressions in italic or bold.
+-	**0.22** -- 27 Dec 2007
+	-	Fix for crash when processing blocks with a percent sign in them.
+-	**0.21** -- 27 Dec 2007
+	- 	Fix for combined strong and emphasis tags
+-	**0.20** -- 13 Oct 2007
+	-	Fix for < as well in image titles, now matches Dingus behavior
+-	**0.19** -- 28 Sep 2007
+	-	Fix for quotation marks " and ampersands & in link and image titles.
+-	**0.18** -- 28 Jul 2007
+	-	Does not crash on unmatched tags (behaves like standard markdown)
+-	**0.17** -- 12 Apr 2007
+	-	Fix for links with %20 in them.
+-	**0.16** -- 12 Apr 2007
+	-	Do not require arg global to exist.
+-	**0.15** -- 28 Aug 2006
+	-	Better handling of links with underscores in them.
+-	**0.14** -- 22 Aug 2006
+	-	Bug for *`foo()`*
+-	**0.13** -- 12 Aug 2006
+	-	Added -l option for including stylesheet inline in document.
+	-	Fixed bug in -s flag.
+	-	Fixed emphasis bug.
+-	**0.12** -- 15 May 2006
+	-	Fixed several bugs to comply with MarkdownTest 1.0 
+-	**0.11** -- 12 May 2006
+	-	Fixed bug for escaping `*` and `_` inside code spans.
+	-	Added license terms.
+	-	Changed join() to table.concat().
+-	**0.10** -- 3 May 2006
+	-	Initial public release.
+
+// Niklas
+]]
+
+
+-- Set up a table for holding local functions to avoid polluting the global namespace
+local M = {}
+local unpack = unpack or table.unpack
+local MT = {__index = _G}
+setmetatable(M, MT)
+
+----------------------------------------------------------------------
+-- Utility functions
+----------------------------------------------------------------------
+
+-- Locks table t from changes, writes an error if someone attempts to change the table.
+-- This is useful for detecting variables that have "accidently" been made global. Something
+-- I tend to do all too much.
+function M.lock(t)
+	local function lock_new_index(t, k, v)
+		error("module has been locked -- " .. k .. " must be declared local", 2)
+	end
+
+	local mt = {__newindex = lock_new_index}
+	if getmetatable(t) then
+      mt.__index = getmetatable(t).__index
+   end
+	setmetatable(t, mt)
+end
+
+-- Returns the result of mapping the values in table t through the function f
+local function map(t, f)
+	local out = {}
+	for k,v in pairs(t) do out[k] = f(v,k) end
+	return out
+end
+
+-- The identity function, useful as a placeholder.
+local function identity(text) return text end
+
+-- Functional style if statement. (NOTE: no short circuit evaluation)
+local function iff(t, a, b) if t then return a else return b end end
+
+-- Splits the text into an array of separate lines.
+local function split(text, sep)
+	sep = sep or "\n"
+	local lines = {}
+	local pos = 1
+	while true do
+		local b,e = text:find(sep, pos)
+		if not b then table.insert(lines, text:sub(pos)) break end
+		table.insert(lines, text:sub(pos, b-1))
+		pos = e + 1
+	end
+	return lines
+end
+
+-- Converts tabs to spaces
+local function detab(text)
+	local tab_width = 4
+	local function rep(match)
+		local spaces = -match:len()
+		while spaces<1 do spaces = spaces + tab_width end
+		return match .. string.rep(" ", spaces)
+	end
+	text = text:gsub("([^\n]-)\t", rep)
+	return text
+end
+
+-- Applies string.find for every pattern in the list and returns the first match
+local function find_first(s, patterns, index)
+	local res = {}
+	for _,p in ipairs(patterns) do
+		local match = {s:find(p, index)}
+		if #match>0 and (#res==0 or match[1] < res[1]) then res = match end
+	end
+	return unpack(res)
+end
+
+-- If a replacement array is specified, the range [start, stop] in the array is replaced
+-- with the replacement array and the resulting array is returned. Without a replacement
+-- array the section of the array between start and stop is returned.
+local function splice(array, start, stop, replacement)
+	if replacement then
+		local n = stop - start + 1
+		while n > 0 do
+			table.remove(array, start)
+			n = n - 1
+		end
+		for i,v in ipairs(replacement) do
+			table.insert(array, start, v)
+		end
+		return array
+	else
+		local res = {}
+		for i = start,stop do
+			table.insert(res, array[i])
+		end
+		return res
+	end
+end
+
+-- Outdents the text one step.
+local function outdent(text)
+	text = "\n" .. text
+	text = text:gsub("\n  ? ? ?", "\n")
+	text = text:sub(2)
+	return text
+end
+
+-- Indents the text one step.
+local function indent(text)
+	text = text:gsub("\n", "\n    ")
+	return text
+end
+
+-- Does a simple tokenization of html data. Returns the data as a list of tokens.
+-- Each token is a table with a type field (which is either "tag" or "text") and
+-- a text field (which contains the original token data).
+local function tokenize_html(html)
+	local tokens = {}
+	local pos = 1
+	while true do
+		local start = find_first(html, {"", start)
+		elseif html:match("^<%?", start) then
+			_,stop = html:find("?>", start)
+		else
+			_,stop = html:find("%b<>", start)
+		end
+		if not stop then
+			-- error("Could not match html tag " .. html:sub(start,start+30))
+		 	table.insert(tokens, {type="text", text=html:sub(start, start)})
+			pos = start + 1
+		else
+			table.insert(tokens, {type="tag", text=html:sub(start, stop)})
+			pos = stop + 1
+		end
+	end
+	return tokens
+end
+
+----------------------------------------------------------------------
+-- Hash
+----------------------------------------------------------------------
+
+-- This is used to "hash" data into alphanumeric strings that are unique
+-- in the document. (Note that this is not cryptographic hash, the hash
+-- function is not one-way.) The hash procedure is used to protect parts
+-- of the document from further processing.
+
+local HASH = {
+	-- Has the hash been inited.
+	inited = false,
+
+	-- The unique string prepended to all hash values. This is to ensure
+	-- that hash values do not accidently coincide with an actual existing
+	-- string in the document.
+	identifier = "",
+
+	-- Counter that counts up for each new hash instance.
+	counter = 0,
+
+	-- Hash table.
+	table = {}
+}
+
+-- Inits hashing. Creates a hash_identifier that doesn't occur anywhere
+-- in the text.
+local function init_hash(text)
+	HASH.inited = true
+	HASH.identifier = ""
+	HASH.counter = 0
+	HASH.table = {}
+
+	local s = "HASH"
+	local counter = 0
+	local id
+	while true do
+		id  = s .. counter
+		if not text:find(id, 1, true) then break end
+		counter = counter + 1
+	end
+	HASH.identifier = id
+end
+
+-- Returns the hashed value for s.
+local function hash(s)
+	assert(HASH.inited)
+	if not HASH.table[s] then
+		HASH.counter = HASH.counter + 1
+		local id = HASH.identifier .. HASH.counter .. "X"
+		HASH.table[s] = id
+	end
+	return HASH.table[s]
+end
+
+----------------------------------------------------------------------
+-- Protection
+----------------------------------------------------------------------
+
+-- The protection module is used to "protect" parts of a document
+-- so that they are not modified by subsequent processing steps.
+-- Protected parts are saved in a table for later unprotection
+
+-- Protection data
+local PD = {
+	-- Saved blocks that have been converted
+	blocks = {},
+
+	-- Block level tags that will be protected
+	tags = {"p", "div", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote",
+	"pre", "table", "dl", "ol", "ul", "script", "noscript", "form", "fieldset",
+	"iframe", "math", "ins", "del"}
+}
+
+-- Pattern for matching a block tag that begins and ends in the leftmost
+-- column and may contain indented subtags, i.e.
+-- 
+-- A nested block. +--
+-- Nested data. +--
+--
+local function block_pattern(tag) + return "\n<" .. tag .. ".-\n[ \t]*\n" +end + +-- Pattern for matching a block tag that begins and ends with a newline +local function line_pattern(tag) + return "\n<" .. tag .. ".-[ \t]*\n" +end + +-- Protects the range of characters from start to stop in the text and +-- returns the protected string. +local function protect_range(text, start, stop) + local s = text:sub(start, stop) + local h = hash(s) + PD.blocks[h] = s + text = text:sub(1,start) .. h .. text:sub(stop) + return text +end + +-- Protect every part of the text that matches any of the patterns. The first +-- matching pattern is protected first, etc. +local function protect_matches(text, patterns) + while true do + local start, stop = find_first(text, patterns) + if not start then break end + text = protect_range(text, start, stop) + end + return text +end + +-- Protects blocklevel tags in the specified text +local function protect(text) + -- First protect potentially nested block tags + text = protect_matches(text, map(PD.tags, block_pattern)) + -- Then protect block tags at the line level. + text = protect_matches(text, map(PD.tags, line_pattern)) + -- Protect
and comment tags + text = protect_matches(text, {"\n]->[ \t]*\n"}) + text = protect_matches(text, {"\n[ \t]*\n"}) + return text +end + +-- Returns true if the string s is a hash resulting from protection +local function is_protected(s) + return PD.blocks[s] +end + +-- Unprotects the specified text by expanding all the nonces +local function unprotect(text) + for k,v in pairs(PD.blocks) do + v = v:gsub("%%", "%%%%") + text = text:gsub(k, v) + end + return text +end + + +---------------------------------------------------------------------- +-- Block transform +---------------------------------------------------------------------- + +-- The block transform functions transform the text on the block level. +-- They work with the text as an array of lines rather than as individual +-- characters. + +-- Returns true if the line is a ruler of (char) characters. +-- The line must contain at least three char characters and contain only spaces and +-- char characters. +local function is_ruler_of(line, char) + if not line:match("^[ %" .. char .. "]*$") then return false end + if not line:match("%" .. char .. ".*%" .. char .. ".*%" .. char) then return false end + return true +end + +-- Identifies the block level formatting present in the line +local function classify(line) + local info = {line = line, text = line} + + if line:match("^ ") then + info.type = "indented" + info.outdented = line:sub(5) + return info + end + + for _,c in ipairs({'*', '-', '_', '='}) do + if is_ruler_of(line, c) then + info.type = "ruler" + info.ruler_char = c + return info + end + end + + if line == "" then + info.type = "blank" + return info + end + + if line:match("^(#+)[ \t]*(.-)[ \t]*#*[ \t]*$") then + local m1, m2 = line:match("^(#+)[ \t]*(.-)[ \t]*#*[ \t]*$") + info.type = "header" + info.level = m1:len() + info.text = m2 + return info + end + + if line:match("^ ? ? ?(%d+)%.[ \t]+(.+)") then + local number, text = line:match("^ ? ? ?(%d+)%.[ \t]+(.+)") + info.type = "list_item" + info.list_type = "numeric" + info.number = 0 + number + info.text = text + return info + end + + if line:match("^ ? ? ?([%*%+%-])[ \t]+(.+)") then + local bullet, text = line:match("^ ? ? ?([%*%+%-])[ \t]+(.+)") + info.type = "list_item" + info.list_type = "bullet" + info.bullet = bullet + info.text= text + return info + end + + if line:match("^>[ \t]?(.*)") then + info.type = "blockquote" + info.text = line:match("^>[ \t]?(.*)") + return info + end + + if is_protected(line) then + info.type = "raw" + info.html = unprotect(line) + return info + end + + info.type = "normal" + return info +end + +-- Find headers constisting of a normal line followed by a ruler and converts them to +-- header entries. +local function headers(array) + local i = 1 + while i <= #array - 1 do + if array[i].type == "normal" and array[i+1].type == "ruler" and + (array[i+1].ruler_char == "-" or array[i+1].ruler_char == "=") then + local info = {line = array[i].line} + info.text = info.line + info.type = "header" + info.level = iff(array[i+1].ruler_char == "=", 1, 2) + table.remove(array, i+1) + array[i] = info + end + i = i + 1 + end + return array +end + +local block_transform, blocks_to_html, encode_code, span_transform, encode_backslash_escapes + +-- Find list blocks and convert them to protected data blocks +local function lists(array, sublist) + local function process_list(arr) + local function any_blanks(arr) + for i = 1, #arr do + if arr[i].type == "blank" then return true end + end + return false + end + + local function split_list_items(arr) + local acc = {arr[1]} + local res = {} + for i=2,#arr do + if arr[i].type == "list_item" then + table.insert(res, acc) + acc = {arr[i]} + else + table.insert(acc, arr[i]) + end + end + table.insert(res, acc) + return res + end + + local function process_list_item(lines, block) + while lines[#lines].type == "blank" do + table.remove(lines) + end + + local itemtext = lines[1].text + for i=2,#lines do + itemtext = itemtext .. "\n" .. outdent(lines[i].line) + end + if block then + itemtext = block_transform(itemtext, true) + if not itemtext:find("
") then itemtext = indent(itemtext) end
+				return "    
  • " .. itemtext .. "
  • " + else + local lines = split(itemtext) + lines = map(lines, classify) + lines = lists(lines, true) + lines = blocks_to_html(lines, true) + itemtext = table.concat(lines, "\n") + if not itemtext:find("
    ") then itemtext = indent(itemtext) end
    +				return "    
  • " .. itemtext .. "
  • " + end + end + + local block_list = any_blanks(arr) + local items = split_list_items(arr) + local out = "" + for _, item in ipairs(items) do + out = out .. process_list_item(item, block_list) .. "\n" + end + if arr[1].list_type == "numeric" then + return "
      \n" .. out .. "
    " + else + return "
      \n" .. out .. "
    " + end + end + + -- Finds the range of lines composing the first list in the array. A list + -- starts with (^ list_item) or (blank list_item) and ends with + -- (blank* $) or (blank normal). + -- + -- A sublist can start with just (list_item) does not need a blank... + local function find_list(array, sublist) + local function find_list_start(array, sublist) + if array[1].type == "list_item" then return 1 end + if sublist then + for i = 1,#array do + if array[i].type == "list_item" then return i end + end + else + for i = 1, #array-1 do + if array[i].type == "blank" and array[i+1].type == "list_item" then + return i+1 + end + end + end + return nil + end + local function find_list_end(array, start) + local pos = #array + for i = start, #array-1 do + if array[i].type == "blank" and array[i+1].type ~= "list_item" + and array[i+1].type ~= "indented" and array[i+1].type ~= "blank" then + pos = i-1 + break + end + end + while pos > start and array[pos].type == "blank" do + pos = pos - 1 + end + return pos + end + + local start = find_list_start(array, sublist) + if not start then return nil end + return start, find_list_end(array, start) + end + + while true do + local start, stop = find_list(array, sublist) + if not start then break end + local text = process_list(splice(array, start, stop)) + local info = { + line = text, + type = "raw", + html = text + } + array = splice(array, start, stop, {info}) + end + + -- Convert any remaining list items to normal + for _,line in ipairs(array) do + if line.type == "list_item" then line.type = "normal" end + end + + return array +end + +-- Find and convert blockquote markers. +local function blockquotes(lines) + local function find_blockquote(lines) + local start + for i,line in ipairs(lines) do + if line.type == "blockquote" then + start = i + break + end + end + if not start then return nil end + + local stop = #lines + for i = start+1, #lines do + if lines[i].type == "blank" or lines[i].type == "blockquote" then + elseif lines[i].type == "normal" then + if lines[i-1].type == "blank" then stop = i-1 break end + else + stop = i-1 break + end + end + while lines[stop].type == "blank" do stop = stop - 1 end + return start, stop + end + + local function process_blockquote(lines) + local raw = lines[1].text + for i = 2,#lines do + raw = raw .. "\n" .. lines[i].text + end + local bt = block_transform(raw) + if not bt:find("
    ") then bt = indent(bt) end
    +		return "
    \n " .. bt .. + "\n
    " + end + + while true do + local start, stop = find_blockquote(lines) + if not start then break end + local text = process_blockquote(splice(lines, start, stop)) + local info = { + line = text, + type = "raw", + html = text + } + lines = splice(lines, start, stop, {info}) + end + return lines +end + +-- Find and convert codeblocks. +local function codeblocks(lines) + local function find_codeblock(lines) + local start + for i,line in ipairs(lines) do + if line.type == "indented" then start = i break end + end + if not start then return nil end + + local stop = #lines + for i = start+1, #lines do + if lines[i].type ~= "indented" and lines[i].type ~= "blank" then + stop = i-1 + break + end + end + while lines[stop].type == "blank" do stop = stop - 1 end + return start, stop + end + + local function process_codeblock(lines) + local raw = detab(encode_code(outdent(lines[1].line))) + for i = 2,#lines do + raw = raw .. "\n" .. detab(encode_code(outdent(lines[i].line))) + end + return "
    " .. raw .. "\n
    " + end + + while true do + local start, stop = find_codeblock(lines) + if not start then break end + local text = process_codeblock(splice(lines, start, stop)) + local info = { + line = text, + type = "raw", + html = text + } + lines = splice(lines, start, stop, {info}) + end + return lines +end + +-- Convert lines to html code +function blocks_to_html(lines, no_paragraphs) + local out = {} + local i = 1 + while i <= #lines do + local line = lines[i] + if line.type == "ruler" then + table.insert(out, "
    ") + elseif line.type == "raw" then + table.insert(out, line.html) + elseif line.type == "normal" then + local s = line.line + + while i+1 <= #lines and lines[i+1].type == "normal" do + i = i + 1 + s = s .. "\n" .. lines[i].line + end + + if no_paragraphs then + table.insert(out, span_transform(s)) + else + table.insert(out, "

    " .. span_transform(s) .. "

    ") + end + elseif line.type == "header" then + local s = "" .. span_transform(line.text) .. "" + table.insert(out, s) + else + table.insert(out, line.line) + end + i = i + 1 + end + return out +end + +-- Perform all the block level transforms +function block_transform(text, sublist) + local lines = split(text) + lines = map(lines, classify) + lines = headers(lines) + lines = lists(lines, sublist) + lines = codeblocks(lines) + lines = blockquotes(lines) + lines = blocks_to_html(lines) + local text = table.concat(lines, "\n") + return text +end + +-- Debug function for printing a line array to see the result +-- of partial transforms. +local function print_lines(lines) + for i, line in ipairs(lines) do + print(i, line.type, line.text or line.line) + end +end + +---------------------------------------------------------------------- +-- Span transform +---------------------------------------------------------------------- + +-- Functions for transforming the text at the span level. + +-- These characters may need to be escaped because they have a special +-- meaning in markdown. +local escape_chars = "'\\`*_{}[]()>#+-.!'" +local escape_table = {} + +local function init_escape_table() + escape_table = {} + for i = 1,#escape_chars do + local c = escape_chars:sub(i,i) + escape_table[c] = hash(c) + end +end + +-- Adds a new escape to the escape table. +local function add_escape(text) + if not escape_table[text] then + escape_table[text] = hash(text) + end + return escape_table[text] +end + +-- Escape characters that should not be disturbed by markdown. +local function escape_special_chars(text) + local tokens = tokenize_html(text) + + local out = "" + for _, token in ipairs(tokens) do + local t = token.text + if token.type == "tag" then + -- In tags, encode * and _ so they don't conflict with their use in markdown. + t = t:gsub("%*", escape_table["*"]) + t = t:gsub("%_", escape_table["_"]) + else + t = encode_backslash_escapes(t) + end + out = out .. t + end + return out +end + +-- Encode backspace-escaped characters in the markdown source. +function encode_backslash_escapes(t) + for i=1,escape_chars:len() do + local c = escape_chars:sub(i,i) + t = t:gsub("\\%" .. c, escape_table[c]) + end + return t +end + +-- Unescape characters that have been encoded. +local function unescape_special_chars(t) + local tin = t + for k,v in pairs(escape_table) do + k = k:gsub("%%", "%%%%") + t = t:gsub(v,k) + end + if t ~= tin then t = unescape_special_chars(t) end + return t +end + +-- Encode/escape certain characters inside Markdown code runs. +-- The point is that in code, these characters are literals, +-- and lose their special Markdown meanings. +function encode_code(s) + s = s:gsub("%&", "&") + s = s:gsub("<", "<") + s = s:gsub(">", ">") + for k,v in pairs(escape_table) do + s = s:gsub("%"..k, v) + end + return s +end + +-- Handle backtick blocks. +local function code_spans(s) + s = s:gsub("\\\\", escape_table["\\"]) + s = s:gsub("\\`", escape_table["`"]) + + local pos = 1 + while true do + local start, stop = s:find("`+", pos) + if not start then return s end + local count = stop - start + 1 + -- Find a matching numbert of backticks + local estart, estop = s:find(string.rep("`", count), stop+1) + local brstart = s:find("\n", stop+1) + if estart and (not brstart or estart < brstart) then + local code = s:sub(stop+1, estart-1) + code = code:gsub("^[ \t]+", "") + code = code:gsub("[ \t]+$", "") + code = code:gsub(escape_table["\\"], escape_table["\\"] .. escape_table["\\"]) + code = code:gsub(escape_table["`"], escape_table["\\"] .. escape_table["`"]) + code = "" .. encode_code(code) .. "" + code = add_escape(code) + s = s:sub(1, start-1) .. code .. s:sub(estop+1) + pos = start + code:len() + else + pos = stop + 1 + end + end + return s +end + +-- Encode alt text... enodes &, and ". +local function encode_alt(s) + if not s then return s end + s = s:gsub('&', '&') + s = s:gsub('"', '"') + s = s:gsub('<', '<') + return s +end + +local link_database + +-- Handle image references +local function images(text) + local function reference_link(alt, id) + alt = encode_alt(alt:match("%b[]"):sub(2,-2)) + id = id:match("%[(.*)%]"):lower() + if id == "" then id = text:lower() end + link_database[id] = link_database[id] or {} + if not link_database[id].url then return nil end + local url = link_database[id].url or id + url = encode_alt(url) + local title = encode_alt(link_database[id].title) + if title then title = " title=\"" .. title .. "\"" else title = "" end + return add_escape ('' .. alt .. '") + end + + local function inline_link(alt, link) + alt = encode_alt(alt:match("%b[]"):sub(2,-2)) + local url, title = link:match("%(?[ \t]*['\"](.+)['\"]") + url = url or link:match("%(?%)") + url = encode_alt(url) + title = encode_alt(title) + if title then + return add_escape('' .. alt .. '') + else + return add_escape('' .. alt .. '') + end + end + + text = text:gsub("!(%b[])[ \t]*\n?[ \t]*(%b[])", reference_link) + text = text:gsub("!(%b[])(%b())", inline_link) + return text +end + +-- Handle anchor references +local function anchors(text) + local function reference_link(text, id) + text = text:match("%b[]"):sub(2,-2) + id = id:match("%b[]"):sub(2,-2):lower() + if id == "" then id = text:lower() end + link_database[id] = link_database[id] or {} + if not link_database[id].url then return nil end + local url = link_database[id].url or id + url = encode_alt(url) + local title = encode_alt(link_database[id].title) + if title then title = " title=\"" .. title .. "\"" else title = "" end + return add_escape("") .. text .. add_escape("") + end + + local function inline_link(text, link) + text = text:match("%b[]"):sub(2,-2) + local url, title = link:match("%(?[ \t]*['\"](.+)['\"]") + title = encode_alt(title) + url = url or link:match("%(?%)") or "" + url = encode_alt(url) + if title then + return add_escape("") .. text .. "" + else + return add_escape("") .. text .. add_escape("") + end + end + + text = text:gsub("(%b[])[ \t]*\n?[ \t]*(%b[])", reference_link) + text = text:gsub("(%b[])(%b())", inline_link) + return text +end + +-- Handle auto links, i.e. . +local function auto_links(text) + local function link(s) + return add_escape("") .. s .. "" + end + -- Encode chars as a mix of dec and hex entitites to (perhaps) fool + -- spambots. + local function encode_email_address(s) + -- Use a deterministic encoding to make unit testing possible. + -- Code 45% hex, 45% dec, 10% plain. + local hex = {code = function(c) return "&#x" .. string.format("%x", c:byte()) .. ";" end, count = 1, rate = 0.45} + local dec = {code = function(c) return "&#" .. c:byte() .. ";" end, count = 0, rate = 0.45} + local plain = {code = function(c) return c end, count = 0, rate = 0.1} + local codes = {hex, dec, plain} + local function swap(t,k1,k2) local temp = t[k2] t[k2] = t[k1] t[k1] = temp end + + local out = "" + for i = 1,s:len() do + for _,code in ipairs(codes) do code.count = code.count + code.rate end + if codes[1].count < codes[2].count then swap(codes,1,2) end + if codes[2].count < codes[3].count then swap(codes,2,3) end + if codes[1].count < codes[2].count then swap(codes,1,2) end + + local code = codes[1] + local c = s:sub(i,i) + -- Force encoding of "@" to make email address more invisible. + if c == "@" and code == plain then code = codes[2] end + out = out .. code.code(c) + code.count = code.count - 1 + end + return out + end + local function mail(s) + s = unescape_special_chars(s) + local address = encode_email_address("mailto:" .. s) + local text = encode_email_address(s) + return add_escape("") .. text .. "" + end + -- links + text = text:gsub("<(https?:[^'\">%s]+)>", link) + text = text:gsub("<(ftp:[^'\">%s]+)>", link) + + -- mail + text = text:gsub("%s]+)>", mail) + text = text:gsub("<([-.%w]+%@[-.%w]+)>", mail) + return text +end + +-- Encode free standing amps (&) and angles (<)... note that this does not +-- encode free >. +local function amps_and_angles(s) + -- encode amps not part of &..; expression + local pos = 1 + while true do + local amp = s:find("&", pos) + if not amp then break end + local semi = s:find(";", amp+1) + local stop = s:find("[ \t\n&]", amp+1) + if not semi or (stop and stop < semi) or (semi - amp) > 15 then + s = s:sub(1,amp-1) .. "&" .. s:sub(amp+1) + pos = amp+1 + else + pos = amp+1 + end + end + + -- encode naked <'s + s = s:gsub("<([^a-zA-Z/?$!])", "<%1") + s = s:gsub("<$", "<") + + -- what about >, nothing done in the original markdown source to handle them + return s +end + +-- Handles emphasis markers (* and _) in the text. +local function emphasis(text) + for _, s in ipairs {"%*%*", "%_%_"} do + text = text:gsub(s .. "([^%s][%*%_]?)" .. s, "%1") + text = text:gsub(s .. "([^%s][^<>]-[^%s][%*%_]?)" .. s, "%1") + end + for _, s in ipairs {"%*", "%_"} do + text = text:gsub(s .. "([^%s_])" .. s, "%1") + text = text:gsub(s .. "([^%s_])" .. s, "%1") + text = text:gsub(s .. "([^%s_][^<>_]-[^%s_])" .. s, "%1") + text = text:gsub(s .. "([^<>_]-[^<>_]-[^<>_]-)" .. s, "%1") + end + return text +end + +-- Handles line break markers in the text. +local function line_breaks(text) + return text:gsub(" +\n", "
    \n") +end + +-- Perform all span level transforms. +function span_transform(text) + text = code_spans(text) + text = escape_special_chars(text) + text = images(text) + text = anchors(text) + text = auto_links(text) + text = amps_and_angles(text) + text = emphasis(text) + text = line_breaks(text) + return text +end + +---------------------------------------------------------------------- +-- Markdown +---------------------------------------------------------------------- + +-- Cleanup the text by normalizing some possible variations to make further +-- processing easier. +local function cleanup(text) + -- Standardize line endings + text = text:gsub("\r\n", "\n") -- DOS to UNIX + text = text:gsub("\r", "\n") -- Mac to UNIX + + -- Convert all tabs to spaces + text = detab(text) + + -- Strip lines with only spaces and tabs + while true do + local subs + text, subs = text:gsub("\n[ \t]+\n", "\n\n") + if subs == 0 then break end + end + + return "\n" .. text .. "\n" +end + +-- Strips link definitions from the text and stores the data in a lookup table. +local function strip_link_definitions(text) + local linkdb = {} + + local function link_def(id, url, title) + id = id:match("%[(.+)%]"):lower() + linkdb[id] = linkdb[id] or {} + linkdb[id].url = url or linkdb[id].url + linkdb[id].title = title or linkdb[id].title + return "" + end + + local def_no_title = "\n ? ? ?(%b[]):[ \t]*\n?[ \t]*]+)>?[ \t]*" + local def_title1 = def_no_title .. "[ \t]+\n?[ \t]*[\"'(]([^\n]+)[\"')][ \t]*" + local def_title2 = def_no_title .. "[ \t]*\n[ \t]*[\"'(]([^\n]+)[\"')][ \t]*" + local def_title3 = def_no_title .. "[ \t]*\n?[ \t]+[\"'(]([^\n]+)[\"')][ \t]*" + + text = text:gsub(def_title1, link_def) + text = text:gsub(def_title2, link_def) + text = text:gsub(def_title3, link_def) + text = text:gsub(def_no_title, link_def) + return text, linkdb +end + +link_database = {} + +-- Main markdown processing function +local function markdown(text) + init_hash(text) + init_escape_table() + + text = cleanup(text) + text = protect(text) + text, link_database = strip_link_definitions(text) + text = block_transform(text) + text = unescape_special_chars(text) + return text +end + +---------------------------------------------------------------------- +-- End of module +---------------------------------------------------------------------- + +M.lock(M) + +-- Expose markdown function to the world +_G.markdown = M.markdown + +-- Class for parsing command-line options +local OptionParser = {} +OptionParser.__index = OptionParser + +-- Creates a new option parser +function OptionParser:new() + local o = {short = {}, long = {}} + setmetatable(o, self) + return o +end + +-- Calls f() whenever a flag with specified short and long name is encountered +function OptionParser:flag(short, long, f) + local info = {type = "flag", f = f} + if short then self.short[short] = info end + if long then self.long[long] = info end +end + +-- Calls f(param) whenever a parameter flag with specified short and long name is encountered +function OptionParser:param(short, long, f) + local info = {type = "param", f = f} + if short then self.short[short] = info end + if long then self.long[long] = info end +end + +-- Calls f(v) for each non-flag argument +function OptionParser:arg(f) + self.arg = f +end + +-- Runs the option parser for the specified set of arguments. Returns true if all arguments +-- where successfully parsed and false otherwise. +function OptionParser:run(args) + local pos = 1 + local param + while pos <= #args do + local arg = args[pos] + if arg == "--" then + for i=pos+1,#args do + if self.arg then self.arg(args[i]) end + return true + end + end + if arg:match("^%-%-") then + local info = self.long[arg:sub(3)] + if not info then print("Unknown flag: " .. arg) return false end + if info.type == "flag" then + info.f() + pos = pos + 1 + else + param = args[pos+1] + if not param then print("No parameter for flag: " .. arg) return false end + info.f(param) + pos = pos+2 + end + elseif arg:match("^%-") then + for i=2,arg:len() do + local c = arg:sub(i,i) + local info = self.short[c] + if not info then print("Unknown flag: -" .. c) return false end + if info.type == "flag" then + info.f() + else + if i == arg:len() then + param = args[pos+1] + if not param then print("No parameter for flag: -" .. c) return false end + info.f(param) + pos = pos + 1 + else + param = arg:sub(i+1) + info.f(param) + end + break + end + end + pos = pos + 1 + else + if self.arg then self.arg(arg) end + pos = pos + 1 + end + end + return true +end + +-- Handles the case when markdown is run from the command line +local function run_command_line(arg) + -- Generate output for input s given options + local function run(s, options) + s = markdown(s) + if not options.wrap_header then return s end + local header = "" + if options.header then + local f = io.open(options.header) or error("Could not open file: " .. options.header) + header = f:read("*a") + f:close() + else + header = [[ + + + + + TITLE + + + +]] + local title = options.title or s:match("

    (.-)

    ") or s:match("

    (.-)

    ") or + s:match("

    (.-)

    ") or "Untitled" + header = header:gsub("TITLE", title) + if options.inline_style then + local style = "" + local f = io.open(options.stylesheet) + if f then + style = f:read("*a") f:close() + else + error("Could not include style sheet " .. options.stylesheet .. ": File not found") + end + header = header:gsub('', + "") + else + header = header:gsub("STYLESHEET", options.stylesheet) + end + header = header:gsub("CHARSET", options.charset) + end + local footer = "" + if options.footer then + local f = io.open(options.footer) or error("Could not open file: " .. options.footer) + footer = f:read("*a") + f:close() + end + return header .. s .. footer + end + + -- Generate output path name from input path name given options. + local function outpath(path, options) + if options.append then return path .. ".html" end + local m = path:match("^(.+%.html)[^/\\]+$") if m then return m end + m = path:match("^(.+%.)[^/\\]*$") if m and path ~= m .. "html" then return m .. "html" end + return path .. ".html" + end + + -- Default commandline options + local options = { + wrap_header = true, + header = nil, + footer = nil, + charset = "utf-8", + title = nil, + stylesheet = "default.css", + inline_style = false + } + local help = [[ +Usage: markdown.lua [OPTION] [FILE] +Runs the markdown text markup to HTML converter on each file specified on the +command line. If no files are specified, runs on standard input. + +No header: + -n, --no-wrap Don't wrap the output in ... tags. +Custom header: + -e, --header FILE Use content of FILE for header. + -f, --footer FILE Use content of FILE for footer. +Generated header: + -c, --charset SET Specifies charset (default utf-8). + -i, --title TITLE Specifies title (default from first

    tag). + -s, --style STYLE Specifies style sheet file (default default.css). + -l, --inline-style Include the style sheet file inline in the header. +Generated files: + -a, --append Append .html extension (instead of replacing). +Other options: + -h, --help Print this help text. + -t, --test Run the unit tests. +]] + + local run_stdin = true + local op = OptionParser:new() + op:flag("n", "no-wrap", function () options.wrap_header = false end) + op:param("e", "header", function (x) options.header = x end) + op:param("f", "footer", function (x) options.footer = x end) + op:param("c", "charset", function (x) options.charset = x end) + op:param("i", "title", function(x) options.title = x end) + op:param("s", "style", function(x) options.stylesheet = x end) + op:flag("l", "inline-style", function(x) options.inline_style = true end) + op:flag("a", "append", function() options.append = true end) + op:flag("t", "test", function() + local n = arg[0]:gsub("markdown.lua", "markdown-tests.lua") + local f = io.open(n) + if f then + f:close() dofile(n) + else + error("Cannot find markdown-tests.lua") + end + run_stdin = false + end) + op:flag("h", "help", function() print(help) run_stdin = false end) + op:arg(function(path) + local file = io.open(path) or error("Could not open file: " .. path) + local s = file:read("*a") + file:close() + s = run(s, options) + file = io.open(outpath(path, options), "w") or error("Could not open output file: " .. outpath(path, options)) + file:write(s) + file:close() + run_stdin = false + end + ) + + if not op:run(arg) then + print(help) + run_stdin = false + end + + if run_stdin then + local s = io.read("*a") + s = run(s, options) + io.write(s) + end +end + +-- If we are being run from the command-line, act accordingly +if arg and arg[0]:find("markdown%.lua$") then + run_command_line(arg) +else + return markdown +end diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/doc/us/reference.html lua-orbit-2.2.1+dfsg/doc/us/reference.html --- lua-orbit-2.2.0+gita6fb46e+dfsg/doc/us/reference.html 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/doc/us/reference.html 2014-02-02 09:25:01.000000000 +0000 @@ -84,16 +84,16 @@

    app.run(wsapi_env) - WSAPI entry point for application, generated by Orbit

    app.real_path - the root of the application in the filesystem, defaults to -the path inferred by WSAPI launchers (wsapi.app\_path), but can be overriden

    +the path inferred by WSAPI launchers (wsapi.app\_path), but can be overridden

    app.mapper - mapper used by app:model, defaults to an instance of orbit.model, -but can be overriden

    +but can be overridden

    -

    app.not_found - default handler, sends a 404 response to the client, can be overriden. +

    app.not_found - default handler, sends a 404 response to the client, can be overridden. The handler receives a web object

    app.server_error - error handler, sends a 500 response with stack trace to the -client, can be overriden. The handler receives a web object

    +client, can be overridden. The handler receives a web object

    app:dispatch_get(func, patt[, patt...]) - installs func as a GET handler for the patterns patt. func receives a web object and the captures; this handler should return @@ -124,13 +124,13 @@ the extension (default application/octec-stream)

    app:serve_static(web, filename) - returns the content of file filename (which can be -anywher in the filesystem), while setting the appropriate headers with the file's MIME +anywhere in the filesystem), while setting the appropriate headers with the file's MIME type

    app:htmlify(patt[, patt...]) - same as orbit.htmlify, but changes all of the app's module functions that match the patterns patt

    -

    app:model(...) - calls app.mapper:new(...), so the behavior depends on the mapper +

    app:model(...) - calls app.mapper:new(...), so the behaviour depends on the mapper you are using

    web methods

    @@ -178,7 +178,7 @@

    web:empty_param(name) - returns true if input parameter name is empty (as web:empty)

    -

    web:page(name, [env]) - loads and render Orbit page called name. If name starts with / it's relative to +

    web:page(name, [env]) - loads and renders Orbit page called name. If name starts with / it's relative to the document root, otherwise it's relative to the app's path. Returns rendered page. env is an optional environment with extra variables.

    @@ -193,7 +193,7 @@

    a_cache:get(key) - gets value stored in key; usually not used, use the previous function instead

    -

    a_cache:set(key, val) - stores a value in the cache; use a_cache(handler) to encapsulate this behavior

    +

    a_cache:set(key, val) - stores a value in the cache; use a_cache(handler) to encapsulate this behaviour

    a_cache:invalidate(key) - invalidates a cache value

    @@ -212,7 +212,7 @@ that takes no arguments, and wraps it so a new connection is automatically reopened every timeout seconds

    a_mapper:new(name, [tab]) - creates a new model object; name is used together with a_mapper.table_prefix to -form the DB table's name; fields and types are instrospected from the table. tab is an optional table that +form the DB table's name; fields and types are introspected from the table. tab is an optional table that is used as the basis for the model object if present

    a_model.model - the mapper for this model

    @@ -221,7 +221,7 @@

    a_model.driver - the DB driver used

    -

    a_model.meta - metainformation about the model, instorspected from the table

    +

    a_model.meta - metainformation about the model, introspected from the table

    a_model:find(id) - finds and returns the instance of the model with the passed id (keyed using the id column of the table (must be numeric)

    @@ -238,7 +238,7 @@ (args.fields, default is all of them), limit the number of returned rows (args.count), return only distinct rows (args.distinct), and inject fields from other tables (args.inject)

    -

    Example: books:find_first("author = ? and year_pub > ?", { "John Doe", 1995, order = "year_pub asc", count = 5, fields = { "id", "title" } })

    +

    Example: books:find_all("author = ? and year_pub > ?", { "John Doe", 1995, order = "year_pub asc", count = 5, fields = { "id", "title" } })

    a_model:new([tab]) - creates a fresh instance of the model, optionally using tab as initial values

    @@ -251,8 +251,7 @@

    Example: books:find_all_by_author_or_author{ "John Doe", "Jane Doe", order = "year_pub asc" }

    -

    an_instance:save([force_insert]) - saves an instance to the DB, commiting changes or creating the backing record if -the instance is new; if force_insert is true always do an insert instead of an update

    +

    an_instance:save([force_insert]) - saves an instance to the DB, commiting changes or creating the backing record if the instance is new; if force_insert is true always do an insert instead of an update

    If there's a row called created_at this row is set to the creation date of the record; if there's a row called updated_at this row is set to the last update's date.

    diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/doc/us/reference.md lua-orbit-2.2.1+dfsg/doc/us/reference.md --- lua-orbit-2.2.0+gita6fb46e+dfsg/doc/us/reference.md 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/doc/us/reference.md 2014-02-02 09:25:01.000000000 +0000 @@ -17,16 +17,16 @@ **app.run(*wsapi\_env*)** - WSAPI entry point for application, generated by Orbit **app.real\_path** - the root of the application in the filesystem, defaults to -the path inferred by WSAPI launchers (`wsapi.app\_path`), but can be overriden +the path inferred by WSAPI launchers (`wsapi.app\_path`), but can be overridden **app.mapper** - mapper used by `app:model`, defaults to an instance of `orbit.model`, -but can be overriden +but can be overridden -**app.not\_found** - default handler, sends a 404 response to the client, can be overriden. +**app.not\_found** - default handler, sends a 404 response to the client, can be overridden. The handler receives a `web` object **app.server\_error** - error handler, sends a 500 response with stack trace to the -client, can be overriden. The handler receives a `web` object +client, can be overridden. The handler receives a `web` object **app:dispatch\_get(*func*, *patt*[, *patt*...])** - installs *func* as a GET handler for the patterns *patt*. *func* receives a `web` object and the captures; this handler should return @@ -57,13 +57,13 @@ the extension (default application/octec-stream) **app:serve\_static(*web*, *filename*)** - returns the content of file *filename* (which can be -anywher in the filesystem), while setting the appropriate headers with the file's MIME +anywhere in the filesystem), while setting the appropriate headers with the file's MIME type **app:htmlify(*patt*[, *patt*...])** - same as `orbit.htmlify`, but changes all of the app's module functions that match the patterns *patt* -**app:model(...)** - calls `app.mapper:new(...)`, so the behavior depends on the mapper +**app:model(...)** - calls `app.mapper:new(...)`, so the behaviour depends on the mapper you are using ## `web` methods @@ -111,7 +111,7 @@ **web:empty\_param(*name*)** - returns true if input parameter *name* is empty (as web:empty) -**web:page(*name*, [*env*])** - loads and render Orbit page called *name*. If *name* starts with / it's relative to +**web:page(*name*, [*env*])** - loads and renders Orbit page called *name*. If *name* starts with / it's relative to the document root, otherwise it's relative to the app's path. Returns rendered page. *env* is an optional environment with extra variables. @@ -126,7 +126,7 @@ **a\_cache:get(*key*)** - gets value stored in *key*; usually not used, use the previous function instead -**a\_cache:set(*key*, *val*)** - stores a value in the cache; use a\_cache(*handler*) to encapsulate this behavior +**a\_cache:set(*key*, *val*)** - stores a value in the cache; use a\_cache(*handler*) to encapsulate this behaviour **a\_cache:invalidate(*key*)** - invalidates a cache value @@ -145,7 +145,7 @@ that takes no arguments, and wraps it so a new connection is automatically reopened every *timeout* seconds **a\_mapper:new(*name*, [*tab*])** - creates a new model object; *name* is used together with a\_mapper.table\_prefix to -form the DB table's name; fields and types are instrospected from the table. *tab* is an optional table that +form the DB table's name; fields and types are introspected from the table. *tab* is an optional table that is used as the basis for the model object if present **a\_model.model** - the mapper for this model @@ -154,7 +154,7 @@ **a\_model.driver** - the DB driver used -**a\_model.meta** - metainformation about the model, instorspected from the table +**a\_model.meta** - metainformation about the model, introspected from the table **a\_model:find(*id*)** - finds and returns the instance of the model with the passed *id* (keyed using the `id` column of the table (must be numeric) @@ -171,7 +171,7 @@ (args.fields, default is all of them), limit the number of returned rows (args.count), return only distinct rows (args.distinct), and inject fields from other tables (args.inject) -Example: `books:find_first("author = ? and year_pub > ?", { "John Doe", 1995, order = "year_pub asc", count = 5, fields = { "id", "title" } })` +Example: `books:find_all("author = ? and year_pub > ?", { "John Doe", 1995, order = "year_pub asc", count = 5, fields = { "id", "title" } })` **a\_model:new([*tab*])** - creates a fresh instance of the model, optionally using *tab* as initial values @@ -184,8 +184,7 @@ Example: `books:find_all_by_author_or_author{ "John Doe", "Jane Doe", order = "year_pub asc" }` -**an\_instance:save([*force\_insert*])** - saves an instance to the DB, commiting changes or creating the backing record if -the instance is new; if *force\_insert* is true always do an insert instead of an update +**an\_instance:save([*force\_insert*])** - saves an instance to the DB, commiting changes or creating the backing record if the instance is new; if *force\_insert* is true always do an insert instead of an update If there's a row called `created_at` this row is set to the creation date of the record; if there's a row called `updated_at` this row is set to the last update's date. diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/rockspec/orbit-2.2.1-1.rockspec lua-orbit-2.2.1+dfsg/rockspec/orbit-2.2.1-1.rockspec --- lua-orbit-2.2.0+gita6fb46e+dfsg/rockspec/orbit-2.2.1-1.rockspec 1970-01-01 00:00:00.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/rockspec/orbit-2.2.1-1.rockspec 2014-02-02 09:25:01.000000000 +0000 @@ -0,0 +1,35 @@ +package = "Orbit" + +version = "2.2.1-1" + +description = { + summary = "MVC for Lua Web Development", + detailed = [[ + Orbit is a library for developing web applications according to + the Model-View-Controller paradigm in Lua. + ]], + license = "MIT/X11", + homepage = "http://www.keplerproject.org/orbit" +} + +dependencies = { 'luafilesystem >= 1.6.2', 'lpeg >= 0.12', 'wsapi-xavante >= 1.6', 'cosmo >= 13.01.30' } + +source = { + -- !! temporary for testing !! + url = "git://github.com/kognix/orbit.git" +} + +build = { + type = "builtin", + modules = { + orbit = "src/orbit.lua", + ["orbit.model"] = "src/orbit/model.lua", + ["orbit.pages"] = "src/orbit/pages.lua", + ["orbit.cache"] = "src/orbit/cache.lua", + ["orbit.ophandler"] = "src/orbit/ophandler.lua", + ["orbit.routes"] = "src/orbit/routes.lua", + }, + install = { bin = { "src/launchers/orbit", "src/launchers/op.cgi", "src/launchers/op.fcgi" } }, + copy_directories = { "doc", "samples", "test" } +} + diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/samples/blog/blog.lua lua-orbit-2.2.1+dfsg/samples/blog/blog.lua --- lua-orbit-2.2.0+gita6fb46e+dfsg/samples/blog/blog.lua 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/samples/blog/blog.lua 2014-02-02 09:25:01.000000000 +0000 @@ -1,23 +1,29 @@ #!/usr/bin/env wsapi.cgi -require "orbit" -require "orbit.cache" -require "markdown" +local orbit = require "orbit" +local orcache = require "orbit.cache" +local markdown = require "markdown" +local wsutil = require "wsapi.util" -- -- Declares that this is module is an Orbit app -- -module("blog", package.seeall, orbit.new) +local blog = setmetatable(orbit.new(), { __index = _G }) +if _VERSION == "Lua 5.2" then + _ENV = blog +else + setfenv(1, blog) +end -- -- Loads configuration data -- -require "blog_config" +wsutil.loadfile("blog_config.lua", blog)() -- -- Initializes DB connection for Orbit's default model mapper -- -require("luasql." .. database.driver) +local luasql = require("luasql." .. database.driver) local env = luasql[database.driver]() mapper.conn = env:connect(unpack(database.conn_data)) mapper.driver = database.driver @@ -340,3 +346,5 @@ -- Adds html functions to the view functions orbit.htmlify(blog, "layout", "_.+", "render_.+") + +return blog diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/samples/blog/blog.ws lua-orbit-2.2.1+dfsg/samples/blog/blog.ws --- lua-orbit-2.2.0+gita6fb46e+dfsg/samples/blog/blog.ws 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/samples/blog/blog.ws 2014-02-02 09:25:01.000000000 +0000 @@ -1,3 +1,2 @@ -require "blog" +return require "blog" -return blog diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/samples/blog/blog_config.lua lua-orbit-2.2.1+dfsg/samples/blog/blog_config.lua --- lua-orbit-2.2.0+gita6fb46e+dfsg/samples/blog/blog_config.lua 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/samples/blog/blog_config.lua 2014-02-02 09:25:01.000000000 +0000 @@ -1,7 +1,4 @@ --- Puts all the definitions below in blog's namespace -module("blog", package.seeall) - blog_title = "Blog" cache_path = "page_cache" @@ -10,7 +7,7 @@ about_blurb = [[This is an example of a blog built using Orbit. You can browse posts and add comments, but to add new posts you have -to go directly to the database. This will be fixed in the future.]] +to go directly to the database. This will be fixed in the future.]] blogroll = { { "http://slashdot.org", "Slashdot"}, @@ -22,10 +19,10 @@ -- use_xsendfile = true database = { --- driver = "mysql", --- conn_data = { "blog", "root", "password" } +-- driver = "mysql", +-- conn_data = { "blog", "root", "password" } driver = "sqlite3", - conn_data = { blog.real_path .. "/blog.db" } + conn_data = { real_path .. "/blog.db" } } recent_count = 7 @@ -151,7 +148,7 @@ function date.en(date) date = os.date("*t", date) return weekdays.en[date.wday] .. ", " .. months.en[date.month] .. " " .. - ordinalize(date.day) .. " " .. date.year + ordinalize(date.day) .. " " .. date.year end function month.en(month) diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/samples/blog/blog_doc.txt lua-orbit-2.2.1+dfsg/samples/blog/blog_doc.txt --- lua-orbit-2.2.0+gita6fb46e+dfsg/samples/blog/blog_doc.txt 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/samples/blog/blog_doc.txt 1970-01-01 00:00:00.000000000 +0000 @@ -1,212 +0,0 @@ -This page documents the Blog example in the `samples/` folder of -[[Orbit]]'s SVN repo. It is in the "literate programming" style, with code and -explantion insterspersed. The Blog example exercises most of the current -features of Orbit, and can be easily changed to suit the needs of -similar dynamic web sites. - -## Initialization - -First let's require Orbit itself and the other libraries that the Blog uses (the -SQLite3 LuaSQL driver and the Markdown parser). After that we declare -that the Blog is a Lua module, and an Orbit application by passing the -`orbit.app` option to `module`. Finally we load the application's configuration -data. - -
    -require "orbit"
    -require "luasql.sqlite3"
    -require "markdown"
    -
    -module("blog", package.seeall, orbit.app)
    -
    -require "blog_config"
    -
    - -`orbit.app` injects quite a lot of stuff in the `blog` module's namespace. -The most important of these are the `add_models`, `add_controllers` and -`add_views` methods that let you define the main functionality of the -application. It also defines a `mapper` variable that Orbit uses to create -the models (Orbit initializes this variable to its default ORM mapper). Finally, -it defines default controllers for 404 and 500 HTTP error codes as the -`not_found` and `server_error` variables, respectively. Override those if you -want custom pages for your application. - -
    -local env = luasql[database.driver]()
    -mapper.conn = env:connect(database.conn_string)
    -
    - -The code above initializes the DB connection for Orbit's default mapper. You need -to do this before creating the models because Orbit's default mapper hits the -database on model creation to fetch the DB metadata. - -Now we are going to define the model part of the application. We do this by -calling `add_models`, passing a table with the models we want to create. -Orbit calls the mapper's `new` method for each model, passing the model -name and the table with your model's methods. - -
    -blog:add_models{
    -
    - -The first model we define is the `post` model. The default mapper will try to find -posts in a table called `blog_post` in the database. The `id` column is assumed -to be the primary key of the table. - -
    -  post = {
    -    find_comments = function (self)
    -      return models.comment:find_all_by_post_id{ self.id }
    -    end,
    -    find_recent = function (self)
    -      return self:find_all("published_at not null",
    -                           { order = "published_at desc",
    -                             count = recent_count })
    -    end,
    -    find_by_month_and_year = function (self, month, year)
    -      local s = os.time({ year = year, month = month, day = 1 })
    -      local e = os.time({ year = year + math.floor(month / 12),
    -                          month = (month % 12) + 1,
    -                          day = 1 })
    -      return self:find_all("published_at >= ? and published_at < ?",
    -                               { s, e, order = "published_at desc" })
    -    end,
    -    find_months = function (self)
    -      local months = {}
    -      local previous_month = {}
    -      local posts = self:find_all({ order = "published_at desc" })
    -      for _, post in ipairs(posts) do
    -        local date = os.date("*t", post.published_at)
    -        if previous_month.month ~= date.month or
    -           previous_month.year ~= date.year then
    -          previous_month = { month = date.month, year = date.year }
    -          months[#months + 1] = previous_month
    -        end
    -      end
    -      return months
    -    end
    -  },
    -
    - -There is no distinction between "class methods" and "instance methods" for models. -You define both of them inside the model table, and it is your responsibility to -not mix them up when you use your models. But this shouldn't be a surprise to Lua -users. In the case of the `post` model, all of the methods are "class methods", more -specifically finders. The default mapper defines a few generic finder methods, and -also creates tailored finders (such as `find_all_by_post_id` used in `find_comments` -on demand. Their use above should be self-explanatory. - -The next model we declare is the `comment` model. It is much simpler, -with no custom finders, but it does have an "instance method" that -we use later in the view part of the application. - -
    -  comment = {
    -    make_link = function (self)
    -      local author = self.author or anonymous_author
    -      if self.url and self.url ~= "" then
    -        return "<a href=\"" .. self.url .. "\">" .. author .. "</a>"
    -      elseif self.email and self.email ~= "" then
    -        return "<a href=\"mailto:" .. self.email .. "\">" .. author .. "</a"
    -      else
    -        return author
    -      end
    -    end
    -  },
    -
    - -Finally the `page` model just needs the default functionality, so we just -declare it as an empty table. - -
    -  page = {}
    -}
    -
    - -Now we are going to define the controllers of the application. In Orbit, each -controller has a list of patterns that Orbit matches against the `PATH_INFO` -to find the correct controller, and http methods that this controller handlers. -Each method receives the running application instance, and any captures -by the pattern. - -
    -blog:add_controllers{
    -
    - -The `index` controller shows all recent posts, and is pretty straightforward. All -GET requests to `/` or `/index` will go to this controller. It just fetches the -required model data from the database, then passes control to the `index` -view along with the model data. - -
    -  index = { "/", "/index",
    -    get = function(self)
    -      local posts = models.post:find_recent()
    -      local months = models.post:find_months()
    -      local pages = models.page:find_all()
    -      self:render("index", { posts = posts, months = months,
    -                    recent = posts, pages = pages })
    -    end
    -  },
    -
    - -The `post` controller shows a single post (and its comments). Any GET requests -to `/post/{post_id}` go to it. It is pretty similar to `index`, as most of the model -data that it has to load is the same (to render the nav bar, menu, and archive links). -Notice how `post` delegates to `not_found` when the post does not exist. - -
    -  post = { "/post/(%d+)",
    -    get = function (self, post_id)
    -      local post = models.post:find(tonumber(post_id))
    -      local recent = models.post:find_recent()
    -      local pages = models.page:find_all()
    -      if post then
    -        post.comments = post:find_comments()
    -        local months = models.post:find_months()
    -        self:render("post", { post = post, months = months,
    -                     recent = recent, pages = pages })
    -      else
    -        self.not_found.get(self)
    -      end
    -    end
    -  },
    -
    - -The `add_comment` model is the biggest, and most complicated, as it has -to handle POST methods. It also does some validation on the input. If the -comment field is empty it delegates back to the `post` controller, along with -a flag that will make the view display the appropriate error message. If not it -creates a new comment model object, fills it up and then writes it to the database. -The comment's `created_at` field is automatically set to the current time by -Orbit's model mapper. The controller also updates the comment count in -the post object. Finally, it redirects to the post page. The redirect avoids double -posting in case the user hits reload. - -
    -  add_comment = { "/post/(%d+)/addcomment",
    -    post = function (self, post_id)
    -      if string.find(self.input.comment, "^%s*$") then
    -        controllers.post.get(self, post_id, true)
    -      else
    -        local comment = models.comment:new()
    -        comment.post_id = tonumber(post_id)
    -        comment.body = markdown(self.input.comment)
    -        if not string.find(self.input.author, "^%s*$") then
    -          comment.author = self.input.author
    -        end
    -        if not string.find(self.input.email, "^%s*$") then
    -          comment.email = self.input.email
    -        end
    -        if not string.find(self.input.url, "^%s*$") then
    -          comment.url = self.input.url
    -        end
    -        comment:save()
    -        local post = models.post:find(tonumber(post_id))
    -        post.n_comments = (post.n_comments or 0) + 1
    -        post:save()
    -        self:redirect("/post/" .. post_id)
    -      end
    -    end
    -  },
    -
    diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/samples/blog/dump.lua lua-orbit-2.2.1+dfsg/samples/blog/dump.lua --- lua-orbit-2.2.0+gita6fb46e+dfsg/samples/blog/dump.lua 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/samples/blog/dump.lua 2014-02-02 09:25:01.000000000 +0000 @@ -1,22 +1,22 @@ -require "luasql.sqlite3" -require "orbit.model" +local luasql = require "luasql.sqlite3" +local orm = require "orbit.model" -local env = luasql.sqlite3() +local env = luasql() local conn = env:connect("blog.db") -local mapper = orbit.model.new("blog_", conn, "sqlite3") +local mapper = orm.new("blog_", conn, "sqlite3") local tables = { "post", "comment", "page" } print [[ -require "luasql.mysql" -require "orbit.model" +local luasql = require "luasql.mysql" +local orm = require "orbit.model" -local env = luasql.mysql() +local env = luasql() local conn = env:connect("blog", "root", "password") -local mapper = orbit.model.new("blog_", conn, "mysql") +local mapper = orm.new("blog_", conn, "mysql") ]] diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/samples/blog/markdown.lua lua-orbit-2.2.1+dfsg/samples/blog/markdown.lua --- lua-orbit-2.2.0+gita6fb46e+dfsg/samples/blog/markdown.lua 1970-01-01 00:00:00.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/samples/blog/markdown.lua 2014-02-02 09:25:01.000000000 +0000 @@ -0,0 +1,1365 @@ +#!/usr/bin/env lua + +--[[ +# markdown.lua -- version 0.32 + + + +**Author:** Niklas Frykholm, +**Date:** 31 May 2008 + +This is an implementation of the popular text markup language Markdown in pure Lua. +Markdown can convert documents written in a simple and easy to read text format +to well-formatted HTML. For a more thourough description of Markdown and the Markdown +syntax, see . + +The original Markdown source is written in Perl and makes heavy use of advanced +regular expression techniques (such as negative look-ahead, etc) which are not available +in Lua's simple regex engine. Therefore this Lua port has been rewritten from the ground +up. It is probably not completely bug free. If you notice any bugs, please report them to +me. A unit test that exposes the error is helpful. + +## Usage + + require "markdown" + markdown(source) + +``markdown.lua`` exposes a single global function named ``markdown(s)`` which applies the +Markdown transformation to the specified string. + +``markdown.lua`` can also be used directly from the command line: + + lua markdown.lua test.md + +Creates a file ``test.html`` with the converted content of ``test.md``. Run: + + lua markdown.lua -h + +For a description of the command-line options. + +``markdown.lua`` uses the same license as Lua, the MIT license. + +## License + +Copyright © 2008 Niklas Frykholm. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this +software and associated documentation files (the "Software"), to deal in the Software +without restriction, including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies +or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +## Version history + +- **0.32** -- 31 May 2008 + - Fix for links containing brackets +- **0.31** -- 1 Mar 2008 + - Fix for link definitions followed by spaces +- **0.30** -- 25 Feb 2008 + - Consistent behavior with Markdown when the same link reference is reused +- **0.29** -- 24 Feb 2008 + - Fix for
     blocks with spaces in them
    +-	**0.28** -- 18 Feb 2008
    +	-	Fix for link encoding
    +-	**0.27** -- 14 Feb 2008
    +	-	Fix for link database links with ()
    +-	**0.26** -- 06 Feb 2008
    +	-	Fix for nested italic and bold markers
    +-	**0.25** -- 24 Jan 2008
    +	-	Fix for encoding of naked <
    +-	**0.24** -- 21 Jan 2008
    +	-	Fix for link behavior.
    +-	**0.23** -- 10 Jan 2008
    +	-	Fix for a regression bug in longer expressions in italic or bold.
    +-	**0.22** -- 27 Dec 2007
    +	-	Fix for crash when processing blocks with a percent sign in them.
    +-	**0.21** -- 27 Dec 2007
    +	- 	Fix for combined strong and emphasis tags
    +-	**0.20** -- 13 Oct 2007
    +	-	Fix for < as well in image titles, now matches Dingus behavior
    +-	**0.19** -- 28 Sep 2007
    +	-	Fix for quotation marks " and ampersands & in link and image titles.
    +-	**0.18** -- 28 Jul 2007
    +	-	Does not crash on unmatched tags (behaves like standard markdown)
    +-	**0.17** -- 12 Apr 2007
    +	-	Fix for links with %20 in them.
    +-	**0.16** -- 12 Apr 2007
    +	-	Do not require arg global to exist.
    +-	**0.15** -- 28 Aug 2006
    +	-	Better handling of links with underscores in them.
    +-	**0.14** -- 22 Aug 2006
    +	-	Bug for *`foo()`*
    +-	**0.13** -- 12 Aug 2006
    +	-	Added -l option for including stylesheet inline in document.
    +	-	Fixed bug in -s flag.
    +	-	Fixed emphasis bug.
    +-	**0.12** -- 15 May 2006
    +	-	Fixed several bugs to comply with MarkdownTest 1.0 
    +-	**0.11** -- 12 May 2006
    +	-	Fixed bug for escaping `*` and `_` inside code spans.
    +	-	Added license terms.
    +	-	Changed join() to table.concat().
    +-	**0.10** -- 3 May 2006
    +	-	Initial public release.
    +
    +// Niklas
    +]]
    +
    +
    +-- Set up a table for holding local functions to avoid polluting the global namespace
    +local M = {}
    +local unpack = unpack or table.unpack
    +local MT = {__index = _G}
    +setmetatable(M, MT)
    +
    +----------------------------------------------------------------------
    +-- Utility functions
    +----------------------------------------------------------------------
    +
    +-- Locks table t from changes, writes an error if someone attempts to change the table.
    +-- This is useful for detecting variables that have "accidently" been made global. Something
    +-- I tend to do all too much.
    +function M.lock(t)
    +	local function lock_new_index(t, k, v)
    +		error("module has been locked -- " .. k .. " must be declared local", 2)
    +	end
    +
    +	local mt = {__newindex = lock_new_index}
    +	if getmetatable(t) then
    +      mt.__index = getmetatable(t).__index
    +   end
    +	setmetatable(t, mt)
    +end
    +
    +-- Returns the result of mapping the values in table t through the function f
    +local function map(t, f)
    +	local out = {}
    +	for k,v in pairs(t) do out[k] = f(v,k) end
    +	return out
    +end
    +
    +-- The identity function, useful as a placeholder.
    +local function identity(text) return text end
    +
    +-- Functional style if statement. (NOTE: no short circuit evaluation)
    +local function iff(t, a, b) if t then return a else return b end end
    +
    +-- Splits the text into an array of separate lines.
    +local function split(text, sep)
    +	sep = sep or "\n"
    +	local lines = {}
    +	local pos = 1
    +	while true do
    +		local b,e = text:find(sep, pos)
    +		if not b then table.insert(lines, text:sub(pos)) break end
    +		table.insert(lines, text:sub(pos, b-1))
    +		pos = e + 1
    +	end
    +	return lines
    +end
    +
    +-- Converts tabs to spaces
    +local function detab(text)
    +	local tab_width = 4
    +	local function rep(match)
    +		local spaces = -match:len()
    +		while spaces<1 do spaces = spaces + tab_width end
    +		return match .. string.rep(" ", spaces)
    +	end
    +	text = text:gsub("([^\n]-)\t", rep)
    +	return text
    +end
    +
    +-- Applies string.find for every pattern in the list and returns the first match
    +local function find_first(s, patterns, index)
    +	local res = {}
    +	for _,p in ipairs(patterns) do
    +		local match = {s:find(p, index)}
    +		if #match>0 and (#res==0 or match[1] < res[1]) then res = match end
    +	end
    +	return unpack(res)
    +end
    +
    +-- If a replacement array is specified, the range [start, stop] in the array is replaced
    +-- with the replacement array and the resulting array is returned. Without a replacement
    +-- array the section of the array between start and stop is returned.
    +local function splice(array, start, stop, replacement)
    +	if replacement then
    +		local n = stop - start + 1
    +		while n > 0 do
    +			table.remove(array, start)
    +			n = n - 1
    +		end
    +		for i,v in ipairs(replacement) do
    +			table.insert(array, start, v)
    +		end
    +		return array
    +	else
    +		local res = {}
    +		for i = start,stop do
    +			table.insert(res, array[i])
    +		end
    +		return res
    +	end
    +end
    +
    +-- Outdents the text one step.
    +local function outdent(text)
    +	text = "\n" .. text
    +	text = text:gsub("\n  ? ? ?", "\n")
    +	text = text:sub(2)
    +	return text
    +end
    +
    +-- Indents the text one step.
    +local function indent(text)
    +	text = text:gsub("\n", "\n    ")
    +	return text
    +end
    +
    +-- Does a simple tokenization of html data. Returns the data as a list of tokens.
    +-- Each token is a table with a type field (which is either "tag" or "text") and
    +-- a text field (which contains the original token data).
    +local function tokenize_html(html)
    +	local tokens = {}
    +	local pos = 1
    +	while true do
    +		local start = find_first(html, {"", start)
    +		elseif html:match("^<%?", start) then
    +			_,stop = html:find("?>", start)
    +		else
    +			_,stop = html:find("%b<>", start)
    +		end
    +		if not stop then
    +			-- error("Could not match html tag " .. html:sub(start,start+30))
    +		 	table.insert(tokens, {type="text", text=html:sub(start, start)})
    +			pos = start + 1
    +		else
    +			table.insert(tokens, {type="tag", text=html:sub(start, stop)})
    +			pos = stop + 1
    +		end
    +	end
    +	return tokens
    +end
    +
    +----------------------------------------------------------------------
    +-- Hash
    +----------------------------------------------------------------------
    +
    +-- This is used to "hash" data into alphanumeric strings that are unique
    +-- in the document. (Note that this is not cryptographic hash, the hash
    +-- function is not one-way.) The hash procedure is used to protect parts
    +-- of the document from further processing.
    +
    +local HASH = {
    +	-- Has the hash been inited.
    +	inited = false,
    +
    +	-- The unique string prepended to all hash values. This is to ensure
    +	-- that hash values do not accidently coincide with an actual existing
    +	-- string in the document.
    +	identifier = "",
    +
    +	-- Counter that counts up for each new hash instance.
    +	counter = 0,
    +
    +	-- Hash table.
    +	table = {}
    +}
    +
    +-- Inits hashing. Creates a hash_identifier that doesn't occur anywhere
    +-- in the text.
    +local function init_hash(text)
    +	HASH.inited = true
    +	HASH.identifier = ""
    +	HASH.counter = 0
    +	HASH.table = {}
    +
    +	local s = "HASH"
    +	local counter = 0
    +	local id
    +	while true do
    +		id  = s .. counter
    +		if not text:find(id, 1, true) then break end
    +		counter = counter + 1
    +	end
    +	HASH.identifier = id
    +end
    +
    +-- Returns the hashed value for s.
    +local function hash(s)
    +	assert(HASH.inited)
    +	if not HASH.table[s] then
    +		HASH.counter = HASH.counter + 1
    +		local id = HASH.identifier .. HASH.counter .. "X"
    +		HASH.table[s] = id
    +	end
    +	return HASH.table[s]
    +end
    +
    +----------------------------------------------------------------------
    +-- Protection
    +----------------------------------------------------------------------
    +
    +-- The protection module is used to "protect" parts of a document
    +-- so that they are not modified by subsequent processing steps.
    +-- Protected parts are saved in a table for later unprotection
    +
    +-- Protection data
    +local PD = {
    +	-- Saved blocks that have been converted
    +	blocks = {},
    +
    +	-- Block level tags that will be protected
    +	tags = {"p", "div", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote",
    +	"pre", "table", "dl", "ol", "ul", "script", "noscript", "form", "fieldset",
    +	"iframe", "math", "ins", "del"}
    +}
    +
    +-- Pattern for matching a block tag that begins and ends in the leftmost
    +-- column and may contain indented subtags, i.e.
    +-- 
    +-- A nested block. +--
    +-- Nested data. +--
    +--
    +local function block_pattern(tag) + return "\n<" .. tag .. ".-\n[ \t]*\n" +end + +-- Pattern for matching a block tag that begins and ends with a newline +local function line_pattern(tag) + return "\n<" .. tag .. ".-[ \t]*\n" +end + +-- Protects the range of characters from start to stop in the text and +-- returns the protected string. +local function protect_range(text, start, stop) + local s = text:sub(start, stop) + local h = hash(s) + PD.blocks[h] = s + text = text:sub(1,start) .. h .. text:sub(stop) + return text +end + +-- Protect every part of the text that matches any of the patterns. The first +-- matching pattern is protected first, etc. +local function protect_matches(text, patterns) + while true do + local start, stop = find_first(text, patterns) + if not start then break end + text = protect_range(text, start, stop) + end + return text +end + +-- Protects blocklevel tags in the specified text +local function protect(text) + -- First protect potentially nested block tags + text = protect_matches(text, map(PD.tags, block_pattern)) + -- Then protect block tags at the line level. + text = protect_matches(text, map(PD.tags, line_pattern)) + -- Protect
    and comment tags + text = protect_matches(text, {"\n]->[ \t]*\n"}) + text = protect_matches(text, {"\n[ \t]*\n"}) + return text +end + +-- Returns true if the string s is a hash resulting from protection +local function is_protected(s) + return PD.blocks[s] +end + +-- Unprotects the specified text by expanding all the nonces +local function unprotect(text) + for k,v in pairs(PD.blocks) do + v = v:gsub("%%", "%%%%") + text = text:gsub(k, v) + end + return text +end + + +---------------------------------------------------------------------- +-- Block transform +---------------------------------------------------------------------- + +-- The block transform functions transform the text on the block level. +-- They work with the text as an array of lines rather than as individual +-- characters. + +-- Returns true if the line is a ruler of (char) characters. +-- The line must contain at least three char characters and contain only spaces and +-- char characters. +local function is_ruler_of(line, char) + if not line:match("^[ %" .. char .. "]*$") then return false end + if not line:match("%" .. char .. ".*%" .. char .. ".*%" .. char) then return false end + return true +end + +-- Identifies the block level formatting present in the line +local function classify(line) + local info = {line = line, text = line} + + if line:match("^ ") then + info.type = "indented" + info.outdented = line:sub(5) + return info + end + + for _,c in ipairs({'*', '-', '_', '='}) do + if is_ruler_of(line, c) then + info.type = "ruler" + info.ruler_char = c + return info + end + end + + if line == "" then + info.type = "blank" + return info + end + + if line:match("^(#+)[ \t]*(.-)[ \t]*#*[ \t]*$") then + local m1, m2 = line:match("^(#+)[ \t]*(.-)[ \t]*#*[ \t]*$") + info.type = "header" + info.level = m1:len() + info.text = m2 + return info + end + + if line:match("^ ? ? ?(%d+)%.[ \t]+(.+)") then + local number, text = line:match("^ ? ? ?(%d+)%.[ \t]+(.+)") + info.type = "list_item" + info.list_type = "numeric" + info.number = 0 + number + info.text = text + return info + end + + if line:match("^ ? ? ?([%*%+%-])[ \t]+(.+)") then + local bullet, text = line:match("^ ? ? ?([%*%+%-])[ \t]+(.+)") + info.type = "list_item" + info.list_type = "bullet" + info.bullet = bullet + info.text= text + return info + end + + if line:match("^>[ \t]?(.*)") then + info.type = "blockquote" + info.text = line:match("^>[ \t]?(.*)") + return info + end + + if is_protected(line) then + info.type = "raw" + info.html = unprotect(line) + return info + end + + info.type = "normal" + return info +end + +-- Find headers constisting of a normal line followed by a ruler and converts them to +-- header entries. +local function headers(array) + local i = 1 + while i <= #array - 1 do + if array[i].type == "normal" and array[i+1].type == "ruler" and + (array[i+1].ruler_char == "-" or array[i+1].ruler_char == "=") then + local info = {line = array[i].line} + info.text = info.line + info.type = "header" + info.level = iff(array[i+1].ruler_char == "=", 1, 2) + table.remove(array, i+1) + array[i] = info + end + i = i + 1 + end + return array +end + +local block_transform, blocks_to_html, encode_code, span_transform, encode_backslash_escapes + +-- Find list blocks and convert them to protected data blocks +local function lists(array, sublist) + local function process_list(arr) + local function any_blanks(arr) + for i = 1, #arr do + if arr[i].type == "blank" then return true end + end + return false + end + + local function split_list_items(arr) + local acc = {arr[1]} + local res = {} + for i=2,#arr do + if arr[i].type == "list_item" then + table.insert(res, acc) + acc = {arr[i]} + else + table.insert(acc, arr[i]) + end + end + table.insert(res, acc) + return res + end + + local function process_list_item(lines, block) + while lines[#lines].type == "blank" do + table.remove(lines) + end + + local itemtext = lines[1].text + for i=2,#lines do + itemtext = itemtext .. "\n" .. outdent(lines[i].line) + end + if block then + itemtext = block_transform(itemtext, true) + if not itemtext:find("
    ") then itemtext = indent(itemtext) end
    +				return "    
  • " .. itemtext .. "
  • " + else + local lines = split(itemtext) + lines = map(lines, classify) + lines = lists(lines, true) + lines = blocks_to_html(lines, true) + itemtext = table.concat(lines, "\n") + if not itemtext:find("
    ") then itemtext = indent(itemtext) end
    +				return "    
  • " .. itemtext .. "
  • " + end + end + + local block_list = any_blanks(arr) + local items = split_list_items(arr) + local out = "" + for _, item in ipairs(items) do + out = out .. process_list_item(item, block_list) .. "\n" + end + if arr[1].list_type == "numeric" then + return "
      \n" .. out .. "
    " + else + return "
      \n" .. out .. "
    " + end + end + + -- Finds the range of lines composing the first list in the array. A list + -- starts with (^ list_item) or (blank list_item) and ends with + -- (blank* $) or (blank normal). + -- + -- A sublist can start with just (list_item) does not need a blank... + local function find_list(array, sublist) + local function find_list_start(array, sublist) + if array[1].type == "list_item" then return 1 end + if sublist then + for i = 1,#array do + if array[i].type == "list_item" then return i end + end + else + for i = 1, #array-1 do + if array[i].type == "blank" and array[i+1].type == "list_item" then + return i+1 + end + end + end + return nil + end + local function find_list_end(array, start) + local pos = #array + for i = start, #array-1 do + if array[i].type == "blank" and array[i+1].type ~= "list_item" + and array[i+1].type ~= "indented" and array[i+1].type ~= "blank" then + pos = i-1 + break + end + end + while pos > start and array[pos].type == "blank" do + pos = pos - 1 + end + return pos + end + + local start = find_list_start(array, sublist) + if not start then return nil end + return start, find_list_end(array, start) + end + + while true do + local start, stop = find_list(array, sublist) + if not start then break end + local text = process_list(splice(array, start, stop)) + local info = { + line = text, + type = "raw", + html = text + } + array = splice(array, start, stop, {info}) + end + + -- Convert any remaining list items to normal + for _,line in ipairs(array) do + if line.type == "list_item" then line.type = "normal" end + end + + return array +end + +-- Find and convert blockquote markers. +local function blockquotes(lines) + local function find_blockquote(lines) + local start + for i,line in ipairs(lines) do + if line.type == "blockquote" then + start = i + break + end + end + if not start then return nil end + + local stop = #lines + for i = start+1, #lines do + if lines[i].type == "blank" or lines[i].type == "blockquote" then + elseif lines[i].type == "normal" then + if lines[i-1].type == "blank" then stop = i-1 break end + else + stop = i-1 break + end + end + while lines[stop].type == "blank" do stop = stop - 1 end + return start, stop + end + + local function process_blockquote(lines) + local raw = lines[1].text + for i = 2,#lines do + raw = raw .. "\n" .. lines[i].text + end + local bt = block_transform(raw) + if not bt:find("
    ") then bt = indent(bt) end
    +		return "
    \n " .. bt .. + "\n
    " + end + + while true do + local start, stop = find_blockquote(lines) + if not start then break end + local text = process_blockquote(splice(lines, start, stop)) + local info = { + line = text, + type = "raw", + html = text + } + lines = splice(lines, start, stop, {info}) + end + return lines +end + +-- Find and convert codeblocks. +local function codeblocks(lines) + local function find_codeblock(lines) + local start + for i,line in ipairs(lines) do + if line.type == "indented" then start = i break end + end + if not start then return nil end + + local stop = #lines + for i = start+1, #lines do + if lines[i].type ~= "indented" and lines[i].type ~= "blank" then + stop = i-1 + break + end + end + while lines[stop].type == "blank" do stop = stop - 1 end + return start, stop + end + + local function process_codeblock(lines) + local raw = detab(encode_code(outdent(lines[1].line))) + for i = 2,#lines do + raw = raw .. "\n" .. detab(encode_code(outdent(lines[i].line))) + end + return "
    " .. raw .. "\n
    " + end + + while true do + local start, stop = find_codeblock(lines) + if not start then break end + local text = process_codeblock(splice(lines, start, stop)) + local info = { + line = text, + type = "raw", + html = text + } + lines = splice(lines, start, stop, {info}) + end + return lines +end + +-- Convert lines to html code +function blocks_to_html(lines, no_paragraphs) + local out = {} + local i = 1 + while i <= #lines do + local line = lines[i] + if line.type == "ruler" then + table.insert(out, "
    ") + elseif line.type == "raw" then + table.insert(out, line.html) + elseif line.type == "normal" then + local s = line.line + + while i+1 <= #lines and lines[i+1].type == "normal" do + i = i + 1 + s = s .. "\n" .. lines[i].line + end + + if no_paragraphs then + table.insert(out, span_transform(s)) + else + table.insert(out, "

    " .. span_transform(s) .. "

    ") + end + elseif line.type == "header" then + local s = "" .. span_transform(line.text) .. "" + table.insert(out, s) + else + table.insert(out, line.line) + end + i = i + 1 + end + return out +end + +-- Perform all the block level transforms +function block_transform(text, sublist) + local lines = split(text) + lines = map(lines, classify) + lines = headers(lines) + lines = lists(lines, sublist) + lines = codeblocks(lines) + lines = blockquotes(lines) + lines = blocks_to_html(lines) + local text = table.concat(lines, "\n") + return text +end + +-- Debug function for printing a line array to see the result +-- of partial transforms. +local function print_lines(lines) + for i, line in ipairs(lines) do + print(i, line.type, line.text or line.line) + end +end + +---------------------------------------------------------------------- +-- Span transform +---------------------------------------------------------------------- + +-- Functions for transforming the text at the span level. + +-- These characters may need to be escaped because they have a special +-- meaning in markdown. +local escape_chars = "'\\`*_{}[]()>#+-.!'" +local escape_table = {} + +local function init_escape_table() + escape_table = {} + for i = 1,#escape_chars do + local c = escape_chars:sub(i,i) + escape_table[c] = hash(c) + end +end + +-- Adds a new escape to the escape table. +local function add_escape(text) + if not escape_table[text] then + escape_table[text] = hash(text) + end + return escape_table[text] +end + +-- Escape characters that should not be disturbed by markdown. +local function escape_special_chars(text) + local tokens = tokenize_html(text) + + local out = "" + for _, token in ipairs(tokens) do + local t = token.text + if token.type == "tag" then + -- In tags, encode * and _ so they don't conflict with their use in markdown. + t = t:gsub("%*", escape_table["*"]) + t = t:gsub("%_", escape_table["_"]) + else + t = encode_backslash_escapes(t) + end + out = out .. t + end + return out +end + +-- Encode backspace-escaped characters in the markdown source. +function encode_backslash_escapes(t) + for i=1,escape_chars:len() do + local c = escape_chars:sub(i,i) + t = t:gsub("\\%" .. c, escape_table[c]) + end + return t +end + +-- Unescape characters that have been encoded. +local function unescape_special_chars(t) + local tin = t + for k,v in pairs(escape_table) do + k = k:gsub("%%", "%%%%") + t = t:gsub(v,k) + end + if t ~= tin then t = unescape_special_chars(t) end + return t +end + +-- Encode/escape certain characters inside Markdown code runs. +-- The point is that in code, these characters are literals, +-- and lose their special Markdown meanings. +function encode_code(s) + s = s:gsub("%&", "&") + s = s:gsub("<", "<") + s = s:gsub(">", ">") + for k,v in pairs(escape_table) do + s = s:gsub("%"..k, v) + end + return s +end + +-- Handle backtick blocks. +local function code_spans(s) + s = s:gsub("\\\\", escape_table["\\"]) + s = s:gsub("\\`", escape_table["`"]) + + local pos = 1 + while true do + local start, stop = s:find("`+", pos) + if not start then return s end + local count = stop - start + 1 + -- Find a matching numbert of backticks + local estart, estop = s:find(string.rep("`", count), stop+1) + local brstart = s:find("\n", stop+1) + if estart and (not brstart or estart < brstart) then + local code = s:sub(stop+1, estart-1) + code = code:gsub("^[ \t]+", "") + code = code:gsub("[ \t]+$", "") + code = code:gsub(escape_table["\\"], escape_table["\\"] .. escape_table["\\"]) + code = code:gsub(escape_table["`"], escape_table["\\"] .. escape_table["`"]) + code = "" .. encode_code(code) .. "" + code = add_escape(code) + s = s:sub(1, start-1) .. code .. s:sub(estop+1) + pos = start + code:len() + else + pos = stop + 1 + end + end + return s +end + +-- Encode alt text... enodes &, and ". +local function encode_alt(s) + if not s then return s end + s = s:gsub('&', '&') + s = s:gsub('"', '"') + s = s:gsub('<', '<') + return s +end + +local link_database + +-- Handle image references +local function images(text) + local function reference_link(alt, id) + alt = encode_alt(alt:match("%b[]"):sub(2,-2)) + id = id:match("%[(.*)%]"):lower() + if id == "" then id = text:lower() end + link_database[id] = link_database[id] or {} + if not link_database[id].url then return nil end + local url = link_database[id].url or id + url = encode_alt(url) + local title = encode_alt(link_database[id].title) + if title then title = " title=\"" .. title .. "\"" else title = "" end + return add_escape ('' .. alt .. '") + end + + local function inline_link(alt, link) + alt = encode_alt(alt:match("%b[]"):sub(2,-2)) + local url, title = link:match("%(?[ \t]*['\"](.+)['\"]") + url = url or link:match("%(?%)") + url = encode_alt(url) + title = encode_alt(title) + if title then + return add_escape('' .. alt .. '') + else + return add_escape('' .. alt .. '') + end + end + + text = text:gsub("!(%b[])[ \t]*\n?[ \t]*(%b[])", reference_link) + text = text:gsub("!(%b[])(%b())", inline_link) + return text +end + +-- Handle anchor references +local function anchors(text) + local function reference_link(text, id) + text = text:match("%b[]"):sub(2,-2) + id = id:match("%b[]"):sub(2,-2):lower() + if id == "" then id = text:lower() end + link_database[id] = link_database[id] or {} + if not link_database[id].url then return nil end + local url = link_database[id].url or id + url = encode_alt(url) + local title = encode_alt(link_database[id].title) + if title then title = " title=\"" .. title .. "\"" else title = "" end + return add_escape("") .. text .. add_escape("") + end + + local function inline_link(text, link) + text = text:match("%b[]"):sub(2,-2) + local url, title = link:match("%(?[ \t]*['\"](.+)['\"]") + title = encode_alt(title) + url = url or link:match("%(?%)") or "" + url = encode_alt(url) + if title then + return add_escape("") .. text .. "" + else + return add_escape("") .. text .. add_escape("") + end + end + + text = text:gsub("(%b[])[ \t]*\n?[ \t]*(%b[])", reference_link) + text = text:gsub("(%b[])(%b())", inline_link) + return text +end + +-- Handle auto links, i.e. . +local function auto_links(text) + local function link(s) + return add_escape("") .. s .. "" + end + -- Encode chars as a mix of dec and hex entitites to (perhaps) fool + -- spambots. + local function encode_email_address(s) + -- Use a deterministic encoding to make unit testing possible. + -- Code 45% hex, 45% dec, 10% plain. + local hex = {code = function(c) return "&#x" .. string.format("%x", c:byte()) .. ";" end, count = 1, rate = 0.45} + local dec = {code = function(c) return "&#" .. c:byte() .. ";" end, count = 0, rate = 0.45} + local plain = {code = function(c) return c end, count = 0, rate = 0.1} + local codes = {hex, dec, plain} + local function swap(t,k1,k2) local temp = t[k2] t[k2] = t[k1] t[k1] = temp end + + local out = "" + for i = 1,s:len() do + for _,code in ipairs(codes) do code.count = code.count + code.rate end + if codes[1].count < codes[2].count then swap(codes,1,2) end + if codes[2].count < codes[3].count then swap(codes,2,3) end + if codes[1].count < codes[2].count then swap(codes,1,2) end + + local code = codes[1] + local c = s:sub(i,i) + -- Force encoding of "@" to make email address more invisible. + if c == "@" and code == plain then code = codes[2] end + out = out .. code.code(c) + code.count = code.count - 1 + end + return out + end + local function mail(s) + s = unescape_special_chars(s) + local address = encode_email_address("mailto:" .. s) + local text = encode_email_address(s) + return add_escape("") .. text .. "" + end + -- links + text = text:gsub("<(https?:[^'\">%s]+)>", link) + text = text:gsub("<(ftp:[^'\">%s]+)>", link) + + -- mail + text = text:gsub("%s]+)>", mail) + text = text:gsub("<([-.%w]+%@[-.%w]+)>", mail) + return text +end + +-- Encode free standing amps (&) and angles (<)... note that this does not +-- encode free >. +local function amps_and_angles(s) + -- encode amps not part of &..; expression + local pos = 1 + while true do + local amp = s:find("&", pos) + if not amp then break end + local semi = s:find(";", amp+1) + local stop = s:find("[ \t\n&]", amp+1) + if not semi or (stop and stop < semi) or (semi - amp) > 15 then + s = s:sub(1,amp-1) .. "&" .. s:sub(amp+1) + pos = amp+1 + else + pos = amp+1 + end + end + + -- encode naked <'s + s = s:gsub("<([^a-zA-Z/?$!])", "<%1") + s = s:gsub("<$", "<") + + -- what about >, nothing done in the original markdown source to handle them + return s +end + +-- Handles emphasis markers (* and _) in the text. +local function emphasis(text) + for _, s in ipairs {"%*%*", "%_%_"} do + text = text:gsub(s .. "([^%s][%*%_]?)" .. s, "%1") + text = text:gsub(s .. "([^%s][^<>]-[^%s][%*%_]?)" .. s, "%1") + end + for _, s in ipairs {"%*", "%_"} do + text = text:gsub(s .. "([^%s_])" .. s, "%1") + text = text:gsub(s .. "([^%s_])" .. s, "%1") + text = text:gsub(s .. "([^%s_][^<>_]-[^%s_])" .. s, "%1") + text = text:gsub(s .. "([^<>_]-[^<>_]-[^<>_]-)" .. s, "%1") + end + return text +end + +-- Handles line break markers in the text. +local function line_breaks(text) + return text:gsub(" +\n", "
    \n") +end + +-- Perform all span level transforms. +function span_transform(text) + text = code_spans(text) + text = escape_special_chars(text) + text = images(text) + text = anchors(text) + text = auto_links(text) + text = amps_and_angles(text) + text = emphasis(text) + text = line_breaks(text) + return text +end + +---------------------------------------------------------------------- +-- Markdown +---------------------------------------------------------------------- + +-- Cleanup the text by normalizing some possible variations to make further +-- processing easier. +local function cleanup(text) + -- Standardize line endings + text = text:gsub("\r\n", "\n") -- DOS to UNIX + text = text:gsub("\r", "\n") -- Mac to UNIX + + -- Convert all tabs to spaces + text = detab(text) + + -- Strip lines with only spaces and tabs + while true do + local subs + text, subs = text:gsub("\n[ \t]+\n", "\n\n") + if subs == 0 then break end + end + + return "\n" .. text .. "\n" +end + +-- Strips link definitions from the text and stores the data in a lookup table. +local function strip_link_definitions(text) + local linkdb = {} + + local function link_def(id, url, title) + id = id:match("%[(.+)%]"):lower() + linkdb[id] = linkdb[id] or {} + linkdb[id].url = url or linkdb[id].url + linkdb[id].title = title or linkdb[id].title + return "" + end + + local def_no_title = "\n ? ? ?(%b[]):[ \t]*\n?[ \t]*]+)>?[ \t]*" + local def_title1 = def_no_title .. "[ \t]+\n?[ \t]*[\"'(]([^\n]+)[\"')][ \t]*" + local def_title2 = def_no_title .. "[ \t]*\n[ \t]*[\"'(]([^\n]+)[\"')][ \t]*" + local def_title3 = def_no_title .. "[ \t]*\n?[ \t]+[\"'(]([^\n]+)[\"')][ \t]*" + + text = text:gsub(def_title1, link_def) + text = text:gsub(def_title2, link_def) + text = text:gsub(def_title3, link_def) + text = text:gsub(def_no_title, link_def) + return text, linkdb +end + +link_database = {} + +-- Main markdown processing function +local function markdown(text) + init_hash(text) + init_escape_table() + + text = cleanup(text) + text = protect(text) + text, link_database = strip_link_definitions(text) + text = block_transform(text) + text = unescape_special_chars(text) + return text +end + +---------------------------------------------------------------------- +-- End of module +---------------------------------------------------------------------- + +M.lock(M) + +-- Expose markdown function to the world +_G.markdown = M.markdown + +-- Class for parsing command-line options +local OptionParser = {} +OptionParser.__index = OptionParser + +-- Creates a new option parser +function OptionParser:new() + local o = {short = {}, long = {}} + setmetatable(o, self) + return o +end + +-- Calls f() whenever a flag with specified short and long name is encountered +function OptionParser:flag(short, long, f) + local info = {type = "flag", f = f} + if short then self.short[short] = info end + if long then self.long[long] = info end +end + +-- Calls f(param) whenever a parameter flag with specified short and long name is encountered +function OptionParser:param(short, long, f) + local info = {type = "param", f = f} + if short then self.short[short] = info end + if long then self.long[long] = info end +end + +-- Calls f(v) for each non-flag argument +function OptionParser:arg(f) + self.arg = f +end + +-- Runs the option parser for the specified set of arguments. Returns true if all arguments +-- where successfully parsed and false otherwise. +function OptionParser:run(args) + local pos = 1 + local param + while pos <= #args do + local arg = args[pos] + if arg == "--" then + for i=pos+1,#args do + if self.arg then self.arg(args[i]) end + return true + end + end + if arg:match("^%-%-") then + local info = self.long[arg:sub(3)] + if not info then print("Unknown flag: " .. arg) return false end + if info.type == "flag" then + info.f() + pos = pos + 1 + else + param = args[pos+1] + if not param then print("No parameter for flag: " .. arg) return false end + info.f(param) + pos = pos+2 + end + elseif arg:match("^%-") then + for i=2,arg:len() do + local c = arg:sub(i,i) + local info = self.short[c] + if not info then print("Unknown flag: -" .. c) return false end + if info.type == "flag" then + info.f() + else + if i == arg:len() then + param = args[pos+1] + if not param then print("No parameter for flag: -" .. c) return false end + info.f(param) + pos = pos + 1 + else + param = arg:sub(i+1) + info.f(param) + end + break + end + end + pos = pos + 1 + else + if self.arg then self.arg(arg) end + pos = pos + 1 + end + end + return true +end + +-- Handles the case when markdown is run from the command line +local function run_command_line(arg) + -- Generate output for input s given options + local function run(s, options) + s = markdown(s) + if not options.wrap_header then return s end + local header = "" + if options.header then + local f = io.open(options.header) or error("Could not open file: " .. options.header) + header = f:read("*a") + f:close() + else + header = [[ + + + + + TITLE + + + +]] + local title = options.title or s:match("

    (.-)

    ") or s:match("

    (.-)

    ") or + s:match("

    (.-)

    ") or "Untitled" + header = header:gsub("TITLE", title) + if options.inline_style then + local style = "" + local f = io.open(options.stylesheet) + if f then + style = f:read("*a") f:close() + else + error("Could not include style sheet " .. options.stylesheet .. ": File not found") + end + header = header:gsub('', + "") + else + header = header:gsub("STYLESHEET", options.stylesheet) + end + header = header:gsub("CHARSET", options.charset) + end + local footer = "" + if options.footer then + local f = io.open(options.footer) or error("Could not open file: " .. options.footer) + footer = f:read("*a") + f:close() + end + return header .. s .. footer + end + + -- Generate output path name from input path name given options. + local function outpath(path, options) + if options.append then return path .. ".html" end + local m = path:match("^(.+%.html)[^/\\]+$") if m then return m end + m = path:match("^(.+%.)[^/\\]*$") if m and path ~= m .. "html" then return m .. "html" end + return path .. ".html" + end + + -- Default commandline options + local options = { + wrap_header = true, + header = nil, + footer = nil, + charset = "utf-8", + title = nil, + stylesheet = "default.css", + inline_style = false + } + local help = [[ +Usage: markdown.lua [OPTION] [FILE] +Runs the markdown text markup to HTML converter on each file specified on the +command line. If no files are specified, runs on standard input. + +No header: + -n, --no-wrap Don't wrap the output in ... tags. +Custom header: + -e, --header FILE Use content of FILE for header. + -f, --footer FILE Use content of FILE for footer. +Generated header: + -c, --charset SET Specifies charset (default utf-8). + -i, --title TITLE Specifies title (default from first

    tag). + -s, --style STYLE Specifies style sheet file (default default.css). + -l, --inline-style Include the style sheet file inline in the header. +Generated files: + -a, --append Append .html extension (instead of replacing). +Other options: + -h, --help Print this help text. + -t, --test Run the unit tests. +]] + + local run_stdin = true + local op = OptionParser:new() + op:flag("n", "no-wrap", function () options.wrap_header = false end) + op:param("e", "header", function (x) options.header = x end) + op:param("f", "footer", function (x) options.footer = x end) + op:param("c", "charset", function (x) options.charset = x end) + op:param("i", "title", function(x) options.title = x end) + op:param("s", "style", function(x) options.stylesheet = x end) + op:flag("l", "inline-style", function(x) options.inline_style = true end) + op:flag("a", "append", function() options.append = true end) + op:flag("t", "test", function() + local n = arg[0]:gsub("markdown.lua", "markdown-tests.lua") + local f = io.open(n) + if f then + f:close() dofile(n) + else + error("Cannot find markdown-tests.lua") + end + run_stdin = false + end) + op:flag("h", "help", function() print(help) run_stdin = false end) + op:arg(function(path) + local file = io.open(path) or error("Could not open file: " .. path) + local s = file:read("*a") + file:close() + s = run(s, options) + file = io.open(outpath(path, options), "w") or error("Could not open output file: " .. outpath(path, options)) + file:write(s) + file:close() + run_stdin = false + end + ) + + if not op:run(arg) then + print(help) + run_stdin = false + end + + if run_stdin then + local s = io.read("*a") + s = run(s, options) + io.write(s) + end +end + +-- If we are being run from the command-line, act accordingly +if arg and arg[0]:find("markdown%.lua$") then + run_command_line(arg) +else + return markdown +end diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/samples/blog/populate_mysql.lua lua-orbit-2.2.1+dfsg/samples/blog/populate_mysql.lua --- lua-orbit-2.2.0+gita6fb46e+dfsg/samples/blog/populate_mysql.lua 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/samples/blog/populate_mysql.lua 2014-02-02 09:25:01.000000000 +0000 @@ -1,11 +1,11 @@ -require "luasql.mysql" -require "orbit.model" +local luasql = require "luasql.mysql" +local orm = require "orbit.model" -local env = luasql.mysql() +local env = luasql() local conn = env:connect("blog", "root", "") -local mapper = orbit.model.new("blog_", conn, "mysql") +local mapper = orm.new("blog_", conn, "mysql") diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/samples/hello/hello.lua lua-orbit-2.2.1+dfsg/samples/hello/hello.lua --- lua-orbit-2.2.0+gita6fb46e+dfsg/samples/hello/hello.lua 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/samples/hello/hello.lua 2014-02-02 09:25:01.000000000 +0000 @@ -1,6 +1,6 @@ #!/usr/bin/env wsapi.cgi -require"orbit" +local orbit = require "orbit" -- Orbit applications are usually modules, -- orbit.new does the necessary initialization diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/samples/routes/hello.lua lua-orbit-2.2.1+dfsg/samples/routes/hello.lua --- lua-orbit-2.2.0+gita6fb46e+dfsg/samples/routes/hello.lua 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/samples/routes/hello.lua 2014-02-02 09:25:01.000000000 +0000 @@ -1,6 +1,6 @@ #!/usr/bin/env wsapi.cgi -require "orbit" +local orbit = require "orbit" local R = require "orbit.routes" local hello = orbit.new() diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/samples/songs/songs.cgi lua-orbit-2.2.1+dfsg/samples/songs/songs.cgi --- lua-orbit-2.2.0+gita6fb46e+dfsg/samples/songs/songs.cgi 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/samples/songs/songs.cgi 2014-02-02 09:25:01.000000000 +0000 @@ -1,12 +1,12 @@ #!/usr/bin/env lua -require"lfs" +local lfs = require "lfs" lfs.chdir("/home/mascarenhas/work/orbit/samples/songs") -require"wsapi.cgi" +local wscgi = require "wsapi.cgi" -require"songs" +local songs = require "songs" -wsapi.cgi.run(songs.run) +wscgi.run(songs.run) diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/samples/songs/songs.fcgi lua-orbit-2.2.1+dfsg/samples/songs/songs.fcgi --- lua-orbit-2.2.0+gita6fb46e+dfsg/samples/songs/songs.fcgi 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/samples/songs/songs.fcgi 2014-02-02 09:25:01.000000000 +0000 @@ -1,12 +1,12 @@ #!/usr/bin/env lua -require"lfs" +local lfs = require "lfs" lfs.chdir("/home/mascarenhas/work/orbit/samples/songs") -require"wsapi.fastcgi" +local wsfcgi = require "wsapi.fastcgi" -require"songs" +local sontgs = require "songs" -wsapi.fastcgi.run(songs.run) +wsfcgi.run(songs.run) diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/samples/todo/todo.ws lua-orbit-2.2.1+dfsg/samples/todo/todo.ws --- lua-orbit-2.2.0+gita6fb46e+dfsg/samples/todo/todo.ws 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/samples/todo/todo.ws 2014-02-02 09:25:01.000000000 +0000 @@ -1,9 +1,8 @@ #!/usr/bin/env wsapi.cgi -require "orbit" -require "orbit.pages" -require "cosmo" -require "luasql.sqlite3" +local orbit = require "orbit" +local cosmo = require "cosmo" +local luasql = require "luasql.sqlite3" local todo = orbit.new() diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/samples/toycms/dump.lua lua-orbit-2.2.1+dfsg/samples/toycms/dump.lua --- lua-orbit-2.2.0+gita6fb46e+dfsg/samples/toycms/dump.lua 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/samples/toycms/dump.lua 2014-02-02 09:25:01.000000000 +0000 @@ -1,12 +1,12 @@ -require "luasql.sqlite3" -require "orbit.model" +local luasql = require "luasql.sqlite3" +local orm = require "orbit.model" local args = { ... } -local env = luasql.sqlite3() +local env = luasql() local conn = env:connect(args[1] .. ".db") -local mapper = orbit.model.new("toycms_", conn, "sqlite3") +local mapper = orm.new("toycms_", conn, "sqlite3") local tables = { "post", "comment", "user", "section" } @@ -14,13 +14,13 @@ print [[ -require "luasql.mysql" -require "orbit.model" +local luasql = require "luasql.mysql" +local orm = require "orbit.model" -local env = luasql.mysql() +local env = luasql() local conn = env:connect(db, "root", "password") -local mapper = orbit.model.new("toycms_", conn, "mysql") +local mapper = orm.new("toycms_", conn, "mysql") ]] diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/samples/toycms/index.lua lua-orbit-2.2.1+dfsg/samples/toycms/index.lua --- lua-orbit-2.2.0+gita6fb46e+dfsg/samples/toycms/index.lua 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/samples/toycms/index.lua 2014-02-02 09:25:01.000000000 +0000 @@ -1,5 +1,5 @@ #!/usr/bin/env wsapi.cgi -require "toycms" +local toycms = require "toycms" return toycms.run diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/samples/toycms/markdown.lua lua-orbit-2.2.1+dfsg/samples/toycms/markdown.lua --- lua-orbit-2.2.0+gita6fb46e+dfsg/samples/toycms/markdown.lua 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/samples/toycms/markdown.lua 2014-02-02 09:25:01.000000000 +0000 @@ -1,1304 +1,1365 @@ -#!/usr/bin/env lua - ---[[ -# markdown.lua -- version 0.17 - - - -**Author:** Niklas Frykholm, -**Date:** 12 Apr 2007 - -This is an implementation of the popular text markup language Markdown in pure Lua. -Markdown can convert documents written in a simple and easy to read text format -to well-formatted HTML. For a more thourough description of Markdown and the Markdown -syntax, see . - -The original Markdown source is written in Perl and makes heavy use of advanced -regular expression techniques (such as negative look-ahead, etc) which are not available -in Lua's simple regex engine. Therefore this Lua port has been rewritten from the ground -up. It is probably not completely bug free. If you notice any bugs, please report them to -me. A unit test that exposes the error is helpful. - -## Usage - - require "markdown" - markdown(source) - -``markdown.lua`` exposes a single global function named ``markdown(s)`` which applies the -Markdown transformation to the specified string. - -``markdown.lua`` can also be used directly from the command line: - - lua markdown.lua test.md - -Creates a file ``test.html`` with the converted content of ``test.md``. Run: - - lua markdown.lua -h - -For a description of the command-line options. - -``markdown.lua`` uses the same license as Lua, the MIT license. - -## License - -Copyright © 2007 Niklas Frykholm. - -Permission is hereby granted, free of charge, to any person obtaining a copy of this -software and associated documentation files (the "Software"), to deal in the Software -without restriction, including without limitation the rights to use, copy, modify, merge, -publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons -to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies -or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -## Version history - -- **0.17** -- 12 Apr 2007 - - Fix for links with %20 in them. -- **0.16** -- 12 Apr 2007 - - Do not require arg global to exist. -- **0.15** -- 28 Aug 2006 - - Better handling of links with underscores in them. -- **0.14** -- 22 Aug 2006 - - Bug for *`foo()`* -- **0.13** -- 12 Aug 2006 - - Added -l option for including stylesheet inline in document. - - Fixed bug in -s flag. - - Fixed emphasis bug. -- **0.12** -- 15 May 2006 - - Fixed several bugs to comply with MarkdownTest 1.0 -- **0.11** -- 12 May 2006 - - Fixed bug for escaping `*` and `_` inside code spans. - - Added license terms. - - Changed join() to table.concat(). -- **0.10** -- 3 May 2006 - - Initial public release. - -// Niklas -]] - - --- Set up a table for holding local functions to avoid polluting the global namespace -local M = {} -local MT = {__index = _G} -setmetatable(M, MT) -setfenv(1, M) - ----------------------------------------------------------------------- --- Utility functions ----------------------------------------------------------------------- - --- Locks table t from changes, writes an error if someone attempts to change the table. --- This is useful for detecting variables that have "accidently" been made global. Something --- I tend to do all too much. -function lock(t) - function lock_new_index(t, k, v) - error("module has been locked -- " .. k .. " must be declared local", 2) - end - - local mt = {__newindex = lock_new_index} - if getmetatable(t) then mt.__index = getmetatable(t).__index end - setmetatable(t, mt) -end - --- Returns the result of mapping the values in table t through the function f -function map(t, f) - local out = {} - for k,v in pairs(t) do out[k] = f(v,k) end - return out -end - --- The identity function, useful as a placeholder. -function identity(text) return text end - --- Functional style if statement. (NOTE: no short circuit evaluation) -function iff(t, a, b) if t then return a else return b end end - --- Splits the text into an array of separate lines. -function split(text, sep) - sep = sep or "\n" - local lines = {} - local pos = 1 - while true do - local b,e = text:find(sep, pos) - if not b then table.insert(lines, text:sub(pos)) break end - table.insert(lines, text:sub(pos, b-1)) - pos = e + 1 - end - return lines -end - --- Converts tabs to spaces -function detab(text) - local tab_width = 4 - local function rep(match) - local spaces = -match:len() - while spaces<1 do spaces = spaces + tab_width end - return match .. string.rep(" ", spaces) - end - text = text:gsub("([^\n]-)\t", rep) - return text -end - --- Applies string.find for every pattern in the list and returns the first match -function find_first(s, patterns, index) - local res = {} - for _,p in ipairs(patterns) do - local match = {s:find(p, index)} - if #match>0 and (#res==0 or match[1] < res[1]) then res = match end - end - return unpack(res) -end - --- If a replacement array is specified, the range [start, stop] in the array is replaced --- with the replacement array and the resulting array is returned. Without a replacement --- array the section of the array between start and stop is returned. -function splice(array, start, stop, replacement) - if replacement then - local n = stop - start + 1 - while n > 0 do - table.remove(array, start) - n = n - 1 - end - for i,v in ipairs(replacement) do - table.insert(array, start, v) - end - return array - else - local res = {} - for i = start,stop do - table.insert(res, array[i]) - end - return res - end -end - --- Outdents the text one step. -function outdent(text) - text = "\n" .. text - text = text:gsub("\n ? ? ?", "\n") - text = text:sub(2) - return text -end - --- Indents the text one step. -function indent(text) - text = text:gsub("\n", "\n ") - return text -end - --- Does a simple tokenization of html data. Returns the data as a list of tokens. --- Each token is a table with a type field (which is either "tag" or "text") and --- a text field (which contains the original token data). -function tokenize_html(html) - local tokens = {} - local pos = 1 - while true do - local start = find_first(html, {"", start) - elseif html:match("^<%?", start) then - _,stop = html:find("?>", start) - else - _,stop = html:find("%b<>", start) - end - if not stop then error("Could not match html tags") end - table.insert(tokens, {type="tag", text=html:sub(start, stop)}) - pos = stop + 1 - end - return tokens -end - ----------------------------------------------------------------------- --- Hash ----------------------------------------------------------------------- - --- This is used to "hash" data into alphanumeric strings that are unique --- in the document. (Note that this is not cryptographic hash, the hash --- function is not one-way.) The hash procedure is used to protect parts --- of the document from further processing. - -local HASH = { - -- Has the hash been inited. - inited = false, - - -- The unique string prepended to all hash values. This is to ensure - -- that hash values do not accidently coincide with an actual existing - -- string in the document. - identifier = "", - - -- Counter that counts up for each new hash instance. - counter = 0, - - -- Hash table. - table = {} -} - --- Inits hashing. Creates a hash_identifier that doesn't occur anywhere --- in the text. -function init_hash(text) - HASH.inited = true - HASH.identifier = "" - HASH.counter = 0 - HASH.table = {} - - local s = "HASH" - local counter = 0 - local id - while true do - id = s .. counter - if not text:find(id, 1, true) then break end - counter = counter + 1 - end - HASH.identifier = id -end - --- Returns the hashed value for s. -function hash(s) - assert(HASH.inited) - if not HASH.table[s] then - HASH.counter = HASH.counter + 1 - local id = HASH.identifier .. HASH.counter .. "X" - HASH.table[s] = id - end - return HASH.table[s] -end - ----------------------------------------------------------------------- --- Protection ----------------------------------------------------------------------- - --- The protection module is used to "protect" parts of a document --- so that they are not modified by subsequent processing steps. --- Protected parts are saved in a table for later unprotection - --- Protection data -local PD = { - -- Saved blocks that have been converted - blocks = {}, - - -- Block level tags that will be protected - tags = {"p", "div", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote", - "pre", "table", "dl", "ol", "ul", "script", "noscript", "form", "fieldset", - "iframe", "math", "ins", "del"} -} - --- Pattern for matching a block tag that begins and ends in the leftmost --- column and may contain indented subtags, i.e. ---
    --- A nested block. ---
    --- Nested data. ---
    ---
    -function block_pattern(tag) - return "\n<" .. tag .. ".-\n[ \t]*\n" -end - --- Pattern for matching a block tag that begins and ends with a newline -function line_pattern(tag) - return "\n<" .. tag .. ".-[ \t]*\n" -end - --- Protects the range of characters from start to stop in the text and --- returns the protected string. -function protect_range(text, start, stop) - local s = text:sub(start, stop) - local h = hash(s) - PD.blocks[h] = s - text = text:sub(1,start) .. h .. text:sub(stop) - return text -end - --- Protect every part of the text that matches any of the patterns. The first --- matching pattern is protected first, etc. -function protect_matches(text, patterns) - while true do - local start, stop = find_first(text, patterns) - if not start then break end - text = protect_range(text, start, stop) - end - return text -end - --- Protects blocklevel tags in the specified text -function protect(text) - -- First protect potentially nested block tags - text = protect_matches(text, map(PD.tags, block_pattern)) - -- Then protect block tags at the line level. - text = protect_matches(text, map(PD.tags, line_pattern)) - -- Protect
    and comment tags - text = protect_matches(text, {"\n]->[ \t]*\n"}) - text = protect_matches(text, {"\n[ \t]*\n"}) - return text -end - --- Returns true if the string s is a hash resulting from protection -function is_protected(s) - return PD.blocks[s] -end - --- Unprotects the specified text by expanding all the nonces -function unprotect(text) - for k,v in pairs(PD.blocks) do - text = text:gsub(k, v) - end - return text -end - - ----------------------------------------------------------------------- --- Block transform ----------------------------------------------------------------------- - --- The block transform functions transform the text on the block level. --- They work with the text as an array of lines rather than as individual --- characters. - --- Returns true if the line is a ruler of (char) characters. --- The line must contain at least three char characters and contain only spaces and --- char characters. -function is_ruler_of(line, char) - if not line:match("^[ %" .. char .. "]*$") then return false end - if not line:match("%" .. char .. ".*%" .. char .. ".*%" .. char) then return false end - return true -end - --- Identifies the block level formatting present in the line -function classify(line) - local info = {line = line, text = line} - - if line:match("^ ") then - info.type = "indented" - info.outdented = line:sub(5) - return info - end - - for _,c in ipairs({'*', '-', '_', '='}) do - if is_ruler_of(line, c) then - info.type = "ruler" - info.ruler_char = c - return info - end - end - - if line == "" then - info.type = "blank" - return info - end - - if line:match("^(#+)[ \t]*(.-)[ \t]*#*[ \t]*$") then - local m1, m2 = line:match("^(#+)[ \t]*(.-)[ \t]*#*[ \t]*$") - info.type = "header" - info.level = m1:len() - info.text = m2 - return info - end - - if line:match("^ ? ? ?(%d+)%.[ \t]+(.+)") then - local number, text = line:match("^ ? ? ?(%d+)%.[ \t]+(.+)") - info.type = "list_item" - info.list_type = "numeric" - info.number = 0 + number - info.text = text - return info - end - - if line:match("^ ? ? ?([%*%+%-])[ \t]+(.+)") then - local bullet, text = line:match("^ ? ? ?([%*%+%-])[ \t]+(.+)") - info.type = "list_item" - info.list_type = "bullet" - info.bullet = bullet - info.text= text - return info - end - - if line:match("^>[ \t]?(.*)") then - info.type = "blockquote" - info.text = line:match("^>[ \t]?(.*)") - return info - end - - if is_protected(line) then - info.type = "raw" - info.html = unprotect(line) - return info - end - - info.type = "normal" - return info -end - --- Find headers constisting of a normal line followed by a ruler and converts them to --- header entries. -function headers(array) - local i = 1 - while i <= #array - 1 do - if array[i].type == "normal" and array[i+1].type == "ruler" and - (array[i+1].ruler_char == "-" or array[i+1].ruler_char == "=") then - local info = {line = array[i].line} - info.text = info.line - info.type = "header" - info.level = iff(array[i+1].ruler_char == "=", 1, 2) - table.remove(array, i+1) - array[i] = info - end - i = i + 1 - end - return array -end - --- Find list blocks and convert them to protected data blocks -function lists(array, sublist) - local function process_list(arr) - local function any_blanks(arr) - for i = 1, #arr do - if arr[i].type == "blank" then return true end - end - return false - end - - local function split_list_items(arr) - local acc = {arr[1]} - local res = {} - for i=2,#arr do - if arr[i].type == "list_item" then - table.insert(res, acc) - acc = {arr[i]} - else - table.insert(acc, arr[i]) - end - end - table.insert(res, acc) - return res - end - - local function process_list_item(lines, block) - while lines[#lines].type == "blank" do - table.remove(lines) - end - - local itemtext = lines[1].text - for i=2,#lines do - itemtext = itemtext .. "\n" .. outdent(lines[i].line) - end - if block then - itemtext = block_transform(itemtext, true) - return "
  • " .. indent(itemtext) .. "
  • " - else - local lines = split(itemtext) - lines = map(lines, classify) - lines = lists(lines, true) - lines = blocks_to_html(lines, true) - itemtext = table.concat(lines, "\n") - return "
  • " .. indent(itemtext) .. "
  • " - end - end - - local block_list = any_blanks(arr) - local items = split_list_items(arr) - local out = "" - for _, item in ipairs(items) do - out = out .. process_list_item(item, block_list) .. "\n" - end - if arr[1].list_type == "numeric" then - return "
      \n" .. out .. "
    " - else - return "
      \n" .. out .. "
    " - end - end - - -- Finds the range of lines composing the first list in the array. A list - -- starts with (^ list_item) or (blank list_item) and ends with - -- (blank* $) or (blank normal). - -- - -- A sublist can start with just (list_item) does not need a blank... - local function find_list(array, sublist) - local function find_list_start(array, sublist) - if array[1].type == "list_item" then return 1 end - if sublist then - for i = 1,#array do - if array[i].type == "list_item" then return i end - end - else - for i = 1, #array-1 do - if array[i].type == "blank" and array[i+1].type == "list_item" then - return i+1 - end - end - end - return nil - end - local function find_list_end(array, start) - local pos = #array - for i = start, #array-1 do - if array[i].type == "blank" and array[i+1].type ~= "list_item" - and array[i+1].type ~= "indented" and array[i+1].type ~= "blank" then - pos = i-1 - break - end - end - while pos > start and array[pos].type == "blank" do - pos = pos - 1 - end - return pos - end - - local start = find_list_start(array, sublist) - if not start then return nil end - return start, find_list_end(array, start) - end - - while true do - local start, stop = find_list(array, sublist) - if not start then break end - local text = process_list(splice(array, start, stop)) - local info = { - line = text, - type = "raw", - html = text - } - array = splice(array, start, stop, {info}) - end - - -- Convert any remaining list items to normal - for _,line in ipairs(array) do - if line.type == "list_item" then line.type = "normal" end - end - - return array -end - --- Find and convert blockquote markers. -function blockquotes(lines) - local function find_blockquote(lines) - local start - for i,line in ipairs(lines) do - if line.type == "blockquote" then - start = i - break - end - end - if not start then return nil end - - local stop = #lines - for i = start+1, #lines do - if lines[i].type == "blank" or lines[i].type == "blockquote" then - elseif lines[i].type == "normal" then - if lines[i-1].type == "blank" then stop = i-1 break end - else - stop = i-1 break - end - end - while lines[stop].type == "blank" do stop = stop - 1 end - return start, stop - end - - local function process_blockquote(lines) - local raw = lines[1].text - for i = 2,#lines do - raw = raw .. "\n" .. lines[i].text - end - local bt = block_transform(raw) - if not bt:find("
    ") then bt = indent(bt) end
    -		return "
    \n " .. bt .. - "\n
    " - end - - while true do - local start, stop = find_blockquote(lines) - if not start then break end - local text = process_blockquote(splice(lines, start, stop)) - local info = { - line = text, - type = "raw", - html = text - } - lines = splice(lines, start, stop, {info}) - end - return lines -end - --- Find and convert codeblocks. -function codeblocks(lines) - local function find_codeblock(lines) - local start - for i,line in ipairs(lines) do - if line.type == "indented" then start = i break end - end - if not start then return nil end - - local stop = #lines - for i = start+1, #lines do - if lines[i].type ~= "indented" and lines[i].type ~= "blank" then - stop = i-1 - break - end - end - while lines[stop].type == "blank" do stop = stop - 1 end - return start, stop - end - - local function process_codeblock(lines) - local raw = detab(encode_code(outdent(lines[1].line))) - for i = 2,#lines do - raw = raw .. "\n" .. detab(encode_code(outdent(lines[i].line))) - end - return "
    " .. raw .. "\n
    " - end - - while true do - local start, stop = find_codeblock(lines) - if not start then break end - local text = process_codeblock(splice(lines, start, stop)) - local info = { - line = text, - type = "raw", - html = text - } - lines = splice(lines, start, stop, {info}) - end - return lines -end - --- Convert lines to html code -function blocks_to_html(lines, no_paragraphs) - local out = {} - local i = 1 - while i <= #lines do - local line = lines[i] - if line.type == "ruler" then - table.insert(out, "
    ") - elseif line.type == "raw" then - table.insert(out, line.html) - elseif line.type == "normal" then - local s = line.line - - while i+1 <= #lines and lines[i+1].type == "normal" do - i = i + 1 - s = s .. "\n" .. lines[i].line - end - - if no_paragraphs then - table.insert(out, span_transform(s)) - else - table.insert(out, "

    " .. span_transform(s) .. "

    ") - end - elseif line.type == "header" then - local s = "" .. span_transform(line.text) .. "" - table.insert(out, s) - else - table.insert(out, line.line) - end - i = i + 1 - end - return out -end - --- Perform all the block level transforms -function block_transform(text, sublist) - local lines = split(text) - lines = map(lines, classify) - lines = headers(lines) - lines = lists(lines, sublist) - lines = codeblocks(lines) - lines = blockquotes(lines) - lines = blocks_to_html(lines) - local text = table.concat(lines, "\n") - return text -end - --- Debug function for printing a line array to see the result --- of partial transforms. -function print_lines(lines) - for i, line in ipairs(lines) do - print(i, line.type, line.text or line.line) - end -end - ----------------------------------------------------------------------- --- Span transform ----------------------------------------------------------------------- - --- Functions for transforming the text at the span level. - --- These characters may need to be escaped because they have a special --- meaning in markdown. -escape_chars = "'\\`*_{}[]()>#+-.!'" -escape_table = {} - -function init_escape_table() - escape_table = {} - for i = 1,#escape_chars do - local c = escape_chars:sub(i,i) - escape_table[c] = hash(c) - end -end - --- Adds a new escape to the escape table. -function add_escape(text) - if not escape_table[text] then - escape_table[text] = hash(text) - end - return escape_table[text] -end - --- Escape characters that should not be disturbed by markdown. -function escape_special_chars(text) - local tokens = tokenize_html(text) - - local out = "" - for _, token in ipairs(tokens) do - local t = token.text - if token.type == "tag" then - -- In tags, encode * and _ so they don't conflict with their use in markdown. - t = t:gsub("%*", escape_table["*"]) - t = t:gsub("%_", escape_table["_"]) - else - t = encode_backslash_escapes(t) - end - out = out .. t - end - return out -end - --- Encode backspace-escaped characters in the markdown source. -function encode_backslash_escapes(t) - for i=1,escape_chars:len() do - local c = escape_chars:sub(i,i) - t = t:gsub("\\%" .. c, escape_table[c]) - end - return t -end - --- Unescape characters that have been encoded. -function unescape_special_chars(t) - local tin = t - for k,v in pairs(escape_table) do - k = k:gsub("%%", "%%%%") - t = t:gsub(v,k) - end - if t ~= tin then t = unescape_special_chars(t) end - return t -end - --- Encode/escape certain characters inside Markdown code runs. --- The point is that in code, these characters are literals, --- and lose their special Markdown meanings. -function encode_code(s) - s = s:gsub("%&", "&") - s = s:gsub("<", "<") - s = s:gsub(">", ">") - for k,v in pairs(escape_table) do - s = s:gsub("%"..k, v) - end - return s -end - --- Handle backtick blocks. -function code_spans(s) - s = s:gsub("\\\\", escape_table["\\"]) - s = s:gsub("\\`", escape_table["`"]) - - local pos = 1 - while true do - local start, stop = s:find("`+", pos) - if not start then return s end - local count = stop - start + 1 - -- Find a matching numbert of backticks - local estart, estop = s:find(string.rep("`", count), stop+1) - local brstart = s:find("\n", stop+1) - if estart and (not brstart or estart < brstart) then - local code = s:sub(stop+1, estart-1) - code = code:gsub("^[ \t]+", "") - code = code:gsub("[ \t]+$", "") - code = code:gsub(escape_table["\\"], escape_table["\\"] .. escape_table["\\"]) - code = code:gsub(escape_table["`"], escape_table["\\"] .. escape_table["`"]) - code = "" .. encode_code(code) .. "" - code = add_escape(code) - s = s:sub(1, start-1) .. code .. s:sub(estop+1) - pos = start + code:len() - else - pos = stop + 1 - end - end - return s -end - --- Handle image references -function images(text) - local function reference_link(alt, id) - alt = alt:match("%[(.*)%]") - id = id:match("%[(.*)%]"):lower() - if id == "" then id = text:lower() end - link_database[id] = link_database[id] or {} - if not link_database[id].url then return nil end - local url = link_database[id].url or id - local title = link_database[id].title - if title then title = " title=\"" .. title .. "\"" else title = "" end - return add_escape ('' .. alt .. '") - end - - local function inline_link(pos, alt, link) - local sides = { r = "right", l = "left" } - local side, pad = pos:match("([rlLR]?)(%d*)") - if side ~= "" then side = "float: " .. sides[side] .. ";" end - if pad ~= "" then pad = "margin: " .. pad .. "px;" end - alt = alt:match("%[(.-)%]") - local url, title = link:match("%(?[ \t]*['\"](.+)['\"]") - url = url or link:match("%(?%)") - if title then - title = title:gsub('"', '"') - return add_escape('' .. alt .. '') - else - return add_escape('' .. alt .. '') - end - end - - text = text:gsub("!(%b[])[ \t]*\n?[ \t]*(%b[])", reference_link) - text = text:gsub("!([lrRL%d]*)(%b[])[ \t]*\n?[ \t]*(%b())", inline_link) - return text -end - --- Handle anchor references -function anchors(text) - local function reference_link(text, id) - text = text:match("%[(.*)%]") - id = id:match("%[(.*)%]"):lower() - if id == "" then id = text:lower() end - link_database[id] = link_database[id] or {} - if not link_database[id].url then return nil end - local url = link_database[id].url or id - local title = link_database[id].title - if title then title = " title=\"" .. title .. "\"" else title = "" end - return add_escape("") .. text .. "" - end - - local function inline_link(text, link) - text = text:match("%[(.-)%]") - local url, title = link:match("%(?[ \t]*['\"](.+)['\"]") - url = url or link:match("%(?%)") or "" - if title then - title = title:gsub('"', '"') - return add_escape("") .. text .. "" - else - return add_escape("") .. text .. "" - end - end - - text = text:gsub("(%b[])[ \t]*\n?[ \t]*(%b[])", reference_link) - text = text:gsub("(%b[])[ \t]*\n?[ \t]*(%b())", inline_link) - return text -end - --- Handle auto links, i.e. . -function auto_links(text) - local function link(s) - return add_escape("") .. s .. "" - end - -- Encode chars as a mix of dec and hex entitites to (perhaps) fool - -- spambots. - local function encode_email_address(s) - -- Use a deterministic encoding to make unit testing possible. - -- Code 45% hex, 45% dec, 10% plain. - local hex = {code = function(c) return "&#x" .. string.format("%x", c:byte()) .. ";" end, count = 1, rate = 0.45} - local dec = {code = function(c) return "&#" .. c:byte() .. ";" end, count = 0, rate = 0.45} - local plain = {code = function(c) return c end, count = 0, rate = 0.1} - local codes = {hex, dec, plain} - local function swap(t,k1,k2) local temp = t[k2] t[k2] = t[k1] t[k1] = temp end - - local out = "" - for i = 1,s:len() do - for _,code in ipairs(codes) do code.count = code.count + code.rate end - if codes[1].count < codes[2].count then swap(codes,1,2) end - if codes[2].count < codes[3].count then swap(codes,2,3) end - if codes[1].count < codes[2].count then swap(codes,1,2) end - - local code = codes[1] - local c = s:sub(i,i) - -- Force encoding of "@" to make email address more invisible. - if c == "@" and code == plain then code = codes[2] end - out = out .. code.code(c) - code.count = code.count - 1 - end - return out - end - local function mail(s) - s = unescape_special_chars(s) - local address = encode_email_address("mailto:" .. s) - local text = encode_email_address(s) - return add_escape("") .. text .. "" - end - -- links - text = text:gsub("<(https?:[^'\">%s]+)>", link) - text = text:gsub("<(ftp:[^'\">%s]+)>", link) - - -- mail - text = text:gsub("%s]+)>", mail) - text = text:gsub("<([-.%w]+%@[-.%w]+)>", mail) - return text -end - --- Encode free standing amps (&) and angles (<)... note that this does not --- encode free >. -function amps_and_angles(s) - -- encode amps not part of &..; expression - local pos = 1 - while true do - local amp = s:find("&", pos) - if not amp then break end - local semi = s:find(";", amp+1) - local stop = s:find("[ \t\n&]", amp+1) - if not semi or (stop and stop < semi) or (semi - amp) > 15 then - s = s:sub(1,amp-1) .. "&" .. s:sub(amp+1) - pos = amp+1 - else - pos = amp+1 - end - end - - -- encode naked <'s - s = s:gsub("<([^a-zA-Z/?$!])", "<%1") - - -- what about >, nothing done in the original markdown source to handle them - return s -end - --- Handles emphasis markers (* and _) in the text. -function emphasis(text) - for _, s in ipairs {"%*%*", "%_%_"} do - text = text:gsub(s .. "([^%s][%*%_]?)" .. s, "%1") - text = text:gsub(s .. "([^%s][^<>]-[^%s][%*%_]?)" .. s, "%1") - end - for _, s in ipairs {"%*", "%_"} do - text = text:gsub(s .. "([^%s])" .. s, "%1") - text = text:gsub(s .. "([^%s][^<>]-[^%s])" .. s, "%1") - end - return text -end - --- Handles line break markers in the text. -function line_breaks(text) - return text:gsub(" +\n", "
    \n") -end - --- Perform all span level transforms. -function span_transform(text) - text = code_spans(text) - text = escape_special_chars(text) - text = images(text) - text = anchors(text) - text = auto_links(text) - text = amps_and_angles(text) - text = emphasis(text) - text = line_breaks(text) - return text -end - ----------------------------------------------------------------------- --- Markdown ----------------------------------------------------------------------- - --- Cleanup the text by normalizing some possible variations to make further --- processing easier. -function cleanup(text) - -- Standardize line endings - text = text:gsub("\r\n", "\n") -- DOS to UNIX - text = text:gsub("\r", "\n") -- Mac to UNIX - - -- Convert all tabs to spaces - text = detab(text) - - -- Strip lines with only spaces and tabs - while true do - local subs - text, subs = text:gsub("\n[ \t]+\n", "\n\n") - if subs == 0 then break end - end - - return "\n" .. text .. "\n" -end - --- Strips link definitions from the text and stores the data in a lookup table. -function strip_link_definitions(text) - local linkdb = {} - - local function link_def(id, url, title) - id = id:match("%[(.+)%]"):lower() - if title then title = title:gsub('"', '"') end - linkdb[id] = {url = url, title = title} - return "" - end - - local def_no_title = "\n ? ? ?(%b[]):[ \t]*\n?[ \t]*]+)>?" - local def_title = def_no_title .. "[ \t]*\n?[ \t]*[\"'(]([^\n]+)[\"')]" - - text = text:gsub(def_title, link_def) - text = text:gsub(def_no_title, link_def) - return text, linkdb -end - -link_database = {} - --- Main markdown processing function -function markdown(text) - init_hash(text) - init_escape_table() - - text = cleanup(text) - text = protect(text) - text, link_database = strip_link_definitions(text) - text = block_transform(text) - text = unescape_special_chars(text) - return text -end - ----------------------------------------------------------------------- --- End of module ----------------------------------------------------------------------- - -setfenv(1, _G) -M.lock(M) - --- Expose markdown function to the world -markdown = M.markdown - --- Class for parsing command-line options -local OptionParser = {} -OptionParser.__index = OptionParser - --- Creates a new option parser -function OptionParser:new() - local o = {short = {}, long = {}} - setmetatable(o, self) - return o -end - --- Calls f() whenever a flag with specified short and long name is encountered -function OptionParser:flag(short, long, f) - local info = {type = "flag", f = f} - if short then self.short[short] = info end - if long then self.long[long] = info end -end - --- Calls f(param) whenever a parameter flag with specified short and long name is encountered -function OptionParser:param(short, long, f) - local info = {type = "param", f = f} - if short then self.short[short] = info end - if long then self.long[long] = info end -end - --- Calls f(v) for each non-flag argument -function OptionParser:arg(f) - self.arg = f -end - --- Runs the option parser for the specified set of arguments. Returns true if all arguments --- where successfully parsed and false otherwise. -function OptionParser:run(args) - local pos = 1 - while pos <= #args do - local arg = args[pos] - if arg == "--" then - for i=pos+1,#args do - if self.arg then self.arg(args[i]) end - return true - end - end - if arg:match("^%-%-") then - local info = self.long[arg:sub(3)] - if not info then print("Unknown flag: " .. arg) return false end - if info.type == "flag" then - info.f() - pos = pos + 1 - else - param = args[pos+1] - if not param then print("No parameter for flag: " .. arg) return false end - info.f(param) - pos = pos+2 - end - elseif arg:match("^%-") then - for i=2,arg:len() do - local c = arg:sub(i,i) - local info = self.short[c] - if not info then print("Unknown flag: -" .. c) return false end - if info.type == "flag" then - info.f() - else - if i == arg:len() then - param = args[pos+1] - if not param then print("No parameter for flag: -" .. c) return false end - info.f(param) - pos = pos + 1 - else - param = arg:sub(i+1) - info.f(param) - end - break - end - end - pos = pos + 1 - else - if self.arg then self.arg(arg) end - pos = pos + 1 - end - end - return true -end - --- Handles the case when markdown is run from the command line -local function run_command_line(arg) - -- Generate output for input s given options - local function run(s, options) - s = markdown(s) - if not options.wrap_header then return s end - local header = "" - if options.header then - local f = io.open(options.header) or error("Could not open file: " .. options.header) - header = f:read("*a") - f:close() - else - header = [[ - - - - - TITLE - - - -]] - local title = options.title or s:match("

    (.-)

    ") or s:match("

    (.-)

    ") or - s:match("

    (.-)

    ") or "Untitled" - header = header:gsub("TITLE", title) - if options.inline_style then - local style = "" - local f = io.open(options.stylesheet) - if f then - style = f:read("*a") f:close() - else - error("Could not include style sheet " .. options.stylesheet .. ": File not found") - end - header = header:gsub('', - "") - else - header = header:gsub("STYLESHEET", options.stylesheet) - end - header = header:gsub("CHARSET", options.charset) - end - local footer = "" - if options.footer then - local f = io.open(options.footer) or error("Could not open file: " .. options.footer) - footer = f:read("*a") - f:close() - end - return header .. s .. footer - end - - -- Generate output path name from input path name given options. - local function outpath(path, options) - if options.append then return path .. ".html" end - local m = path:match("^(.+%.html)[^/\\]+$") if m then return m end - m = path:match("^(.+%.)[^/\\]*$") if m and path ~= m .. "html" then return m .. "html" end - return path .. ".html" - end - - -- Default commandline options - local options = { - wrap_header = true, - header = nil, - footer = nil, - charset = "utf-8", - title = nil, - stylesheet = "default.css", - inline_style = false - } - local help = [[ -Usage: markdown.lua [OPTION] [FILE] -Runs the markdown text markup to HTML converter on each file specified on the -command line. If no files are specified, runs on standard input. - -No header: - -n, --no-wrap Don't wrap the output in ... tags. -Custom header: - -e, --header FILE Use content of FILE for header. - -f, --footer FILE Use content of FILE for footer. -Generated header: - -c, --charset SET Specifies charset (default utf-8). - -i, --title TITLE Specifies title (default from first

    tag). - -s, --style STYLE Specifies style sheet file (default default.css). - -l, --inline-style Include the style sheet file inline in the header. -Generated files: - -a, --append Append .html extension (instead of replacing). -Other options: - -h, --help Print this help text. - -t, --test Run the unit tests. -]] - - local run_stdin = true - local op = OptionParser:new() - op:flag("n", "no-wrap", function () options.wrap_header = false end) - op:param("e", "header", function (x) options.header = x end) - op:param("f", "footer", function (x) options.footer = x end) - op:param("c", "charset", function (x) options.charset = x end) - op:param("i", "title", function(x) options.title = x end) - op:param("s", "style", function(x) options.stylesheet = x end) - op:flag("l", "inline-style", function(x) options.inline_style = true end) - op:flag("a", "append", function() options.append = true end) - op:flag("t", "test", function() - local n = arg[0]:gsub("markdown.lua", "markdown-tests.lua") - local f = io.open(n) - if f then - f:close() dofile(n) - else - error("Cannot find markdown-tests.lua") - end - run_stdin = false - end) - op:flag("h", "help", function() print(help) run_stdin = false end) - op:arg(function(path) - local file = io.open(path) or error("Could not open file: " .. path) - local s = file:read("*a") - file:close() - s = run(s, options) - file = io.open(outpath(path, options), "w") or error("Could not open output file: " .. outpath(path, options)) - file:write(s) - file:close() - run_stdin = false - end - ) - - if not op:run(arg) then - print(help) - run_stdin = false - end - - if run_stdin then - local s = io.read("*a") - s = run(s, options) - io.write(s) - end -end - --- If we are being run from the command-line, act accordingly -if arg and arg[0]:find("markdown%.lua$") then - run_command_line(arg) -else - return markdown -end \ No newline at end of file +#!/usr/bin/env lua + +--[[ +# markdown.lua -- version 0.32 + + + +**Author:** Niklas Frykholm, +**Date:** 31 May 2008 + +This is an implementation of the popular text markup language Markdown in pure Lua. +Markdown can convert documents written in a simple and easy to read text format +to well-formatted HTML. For a more thourough description of Markdown and the Markdown +syntax, see . + +The original Markdown source is written in Perl and makes heavy use of advanced +regular expression techniques (such as negative look-ahead, etc) which are not available +in Lua's simple regex engine. Therefore this Lua port has been rewritten from the ground +up. It is probably not completely bug free. If you notice any bugs, please report them to +me. A unit test that exposes the error is helpful. + +## Usage + + require "markdown" + markdown(source) + +``markdown.lua`` exposes a single global function named ``markdown(s)`` which applies the +Markdown transformation to the specified string. + +``markdown.lua`` can also be used directly from the command line: + + lua markdown.lua test.md + +Creates a file ``test.html`` with the converted content of ``test.md``. Run: + + lua markdown.lua -h + +For a description of the command-line options. + +``markdown.lua`` uses the same license as Lua, the MIT license. + +## License + +Copyright © 2008 Niklas Frykholm. + +Permission is hereby granted, free of charge, to any person obtaining a copy of this +software and associated documentation files (the "Software"), to deal in the Software +without restriction, including without limitation the rights to use, copy, modify, merge, +publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons +to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies +or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. + +## Version history + +- **0.32** -- 31 May 2008 + - Fix for links containing brackets +- **0.31** -- 1 Mar 2008 + - Fix for link definitions followed by spaces +- **0.30** -- 25 Feb 2008 + - Consistent behavior with Markdown when the same link reference is reused +- **0.29** -- 24 Feb 2008 + - Fix for
     blocks with spaces in them
    +-	**0.28** -- 18 Feb 2008
    +	-	Fix for link encoding
    +-	**0.27** -- 14 Feb 2008
    +	-	Fix for link database links with ()
    +-	**0.26** -- 06 Feb 2008
    +	-	Fix for nested italic and bold markers
    +-	**0.25** -- 24 Jan 2008
    +	-	Fix for encoding of naked <
    +-	**0.24** -- 21 Jan 2008
    +	-	Fix for link behavior.
    +-	**0.23** -- 10 Jan 2008
    +	-	Fix for a regression bug in longer expressions in italic or bold.
    +-	**0.22** -- 27 Dec 2007
    +	-	Fix for crash when processing blocks with a percent sign in them.
    +-	**0.21** -- 27 Dec 2007
    +	- 	Fix for combined strong and emphasis tags
    +-	**0.20** -- 13 Oct 2007
    +	-	Fix for < as well in image titles, now matches Dingus behavior
    +-	**0.19** -- 28 Sep 2007
    +	-	Fix for quotation marks " and ampersands & in link and image titles.
    +-	**0.18** -- 28 Jul 2007
    +	-	Does not crash on unmatched tags (behaves like standard markdown)
    +-	**0.17** -- 12 Apr 2007
    +	-	Fix for links with %20 in them.
    +-	**0.16** -- 12 Apr 2007
    +	-	Do not require arg global to exist.
    +-	**0.15** -- 28 Aug 2006
    +	-	Better handling of links with underscores in them.
    +-	**0.14** -- 22 Aug 2006
    +	-	Bug for *`foo()`*
    +-	**0.13** -- 12 Aug 2006
    +	-	Added -l option for including stylesheet inline in document.
    +	-	Fixed bug in -s flag.
    +	-	Fixed emphasis bug.
    +-	**0.12** -- 15 May 2006
    +	-	Fixed several bugs to comply with MarkdownTest 1.0 
    +-	**0.11** -- 12 May 2006
    +	-	Fixed bug for escaping `*` and `_` inside code spans.
    +	-	Added license terms.
    +	-	Changed join() to table.concat().
    +-	**0.10** -- 3 May 2006
    +	-	Initial public release.
    +
    +// Niklas
    +]]
    +
    +
    +-- Set up a table for holding local functions to avoid polluting the global namespace
    +local M = {}
    +local unpack = unpack or table.unpack
    +local MT = {__index = _G}
    +setmetatable(M, MT)
    +
    +----------------------------------------------------------------------
    +-- Utility functions
    +----------------------------------------------------------------------
    +
    +-- Locks table t from changes, writes an error if someone attempts to change the table.
    +-- This is useful for detecting variables that have "accidently" been made global. Something
    +-- I tend to do all too much.
    +function M.lock(t)
    +	local function lock_new_index(t, k, v)
    +		error("module has been locked -- " .. k .. " must be declared local", 2)
    +	end
    +
    +	local mt = {__newindex = lock_new_index}
    +	if getmetatable(t) then
    +      mt.__index = getmetatable(t).__index
    +   end
    +	setmetatable(t, mt)
    +end
    +
    +-- Returns the result of mapping the values in table t through the function f
    +local function map(t, f)
    +	local out = {}
    +	for k,v in pairs(t) do out[k] = f(v,k) end
    +	return out
    +end
    +
    +-- The identity function, useful as a placeholder.
    +local function identity(text) return text end
    +
    +-- Functional style if statement. (NOTE: no short circuit evaluation)
    +local function iff(t, a, b) if t then return a else return b end end
    +
    +-- Splits the text into an array of separate lines.
    +local function split(text, sep)
    +	sep = sep or "\n"
    +	local lines = {}
    +	local pos = 1
    +	while true do
    +		local b,e = text:find(sep, pos)
    +		if not b then table.insert(lines, text:sub(pos)) break end
    +		table.insert(lines, text:sub(pos, b-1))
    +		pos = e + 1
    +	end
    +	return lines
    +end
    +
    +-- Converts tabs to spaces
    +local function detab(text)
    +	local tab_width = 4
    +	local function rep(match)
    +		local spaces = -match:len()
    +		while spaces<1 do spaces = spaces + tab_width end
    +		return match .. string.rep(" ", spaces)
    +	end
    +	text = text:gsub("([^\n]-)\t", rep)
    +	return text
    +end
    +
    +-- Applies string.find for every pattern in the list and returns the first match
    +local function find_first(s, patterns, index)
    +	local res = {}
    +	for _,p in ipairs(patterns) do
    +		local match = {s:find(p, index)}
    +		if #match>0 and (#res==0 or match[1] < res[1]) then res = match end
    +	end
    +	return unpack(res)
    +end
    +
    +-- If a replacement array is specified, the range [start, stop] in the array is replaced
    +-- with the replacement array and the resulting array is returned. Without a replacement
    +-- array the section of the array between start and stop is returned.
    +local function splice(array, start, stop, replacement)
    +	if replacement then
    +		local n = stop - start + 1
    +		while n > 0 do
    +			table.remove(array, start)
    +			n = n - 1
    +		end
    +		for i,v in ipairs(replacement) do
    +			table.insert(array, start, v)
    +		end
    +		return array
    +	else
    +		local res = {}
    +		for i = start,stop do
    +			table.insert(res, array[i])
    +		end
    +		return res
    +	end
    +end
    +
    +-- Outdents the text one step.
    +local function outdent(text)
    +	text = "\n" .. text
    +	text = text:gsub("\n  ? ? ?", "\n")
    +	text = text:sub(2)
    +	return text
    +end
    +
    +-- Indents the text one step.
    +local function indent(text)
    +	text = text:gsub("\n", "\n    ")
    +	return text
    +end
    +
    +-- Does a simple tokenization of html data. Returns the data as a list of tokens.
    +-- Each token is a table with a type field (which is either "tag" or "text") and
    +-- a text field (which contains the original token data).
    +local function tokenize_html(html)
    +	local tokens = {}
    +	local pos = 1
    +	while true do
    +		local start = find_first(html, {"", start)
    +		elseif html:match("^<%?", start) then
    +			_,stop = html:find("?>", start)
    +		else
    +			_,stop = html:find("%b<>", start)
    +		end
    +		if not stop then
    +			-- error("Could not match html tag " .. html:sub(start,start+30))
    +		 	table.insert(tokens, {type="text", text=html:sub(start, start)})
    +			pos = start + 1
    +		else
    +			table.insert(tokens, {type="tag", text=html:sub(start, stop)})
    +			pos = stop + 1
    +		end
    +	end
    +	return tokens
    +end
    +
    +----------------------------------------------------------------------
    +-- Hash
    +----------------------------------------------------------------------
    +
    +-- This is used to "hash" data into alphanumeric strings that are unique
    +-- in the document. (Note that this is not cryptographic hash, the hash
    +-- function is not one-way.) The hash procedure is used to protect parts
    +-- of the document from further processing.
    +
    +local HASH = {
    +	-- Has the hash been inited.
    +	inited = false,
    +
    +	-- The unique string prepended to all hash values. This is to ensure
    +	-- that hash values do not accidently coincide with an actual existing
    +	-- string in the document.
    +	identifier = "",
    +
    +	-- Counter that counts up for each new hash instance.
    +	counter = 0,
    +
    +	-- Hash table.
    +	table = {}
    +}
    +
    +-- Inits hashing. Creates a hash_identifier that doesn't occur anywhere
    +-- in the text.
    +local function init_hash(text)
    +	HASH.inited = true
    +	HASH.identifier = ""
    +	HASH.counter = 0
    +	HASH.table = {}
    +
    +	local s = "HASH"
    +	local counter = 0
    +	local id
    +	while true do
    +		id  = s .. counter
    +		if not text:find(id, 1, true) then break end
    +		counter = counter + 1
    +	end
    +	HASH.identifier = id
    +end
    +
    +-- Returns the hashed value for s.
    +local function hash(s)
    +	assert(HASH.inited)
    +	if not HASH.table[s] then
    +		HASH.counter = HASH.counter + 1
    +		local id = HASH.identifier .. HASH.counter .. "X"
    +		HASH.table[s] = id
    +	end
    +	return HASH.table[s]
    +end
    +
    +----------------------------------------------------------------------
    +-- Protection
    +----------------------------------------------------------------------
    +
    +-- The protection module is used to "protect" parts of a document
    +-- so that they are not modified by subsequent processing steps.
    +-- Protected parts are saved in a table for later unprotection
    +
    +-- Protection data
    +local PD = {
    +	-- Saved blocks that have been converted
    +	blocks = {},
    +
    +	-- Block level tags that will be protected
    +	tags = {"p", "div", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote",
    +	"pre", "table", "dl", "ol", "ul", "script", "noscript", "form", "fieldset",
    +	"iframe", "math", "ins", "del"}
    +}
    +
    +-- Pattern for matching a block tag that begins and ends in the leftmost
    +-- column and may contain indented subtags, i.e.
    +-- 
    +-- A nested block. +--
    +-- Nested data. +--
    +--
    +local function block_pattern(tag) + return "\n<" .. tag .. ".-\n[ \t]*\n" +end + +-- Pattern for matching a block tag that begins and ends with a newline +local function line_pattern(tag) + return "\n<" .. tag .. ".-[ \t]*\n" +end + +-- Protects the range of characters from start to stop in the text and +-- returns the protected string. +local function protect_range(text, start, stop) + local s = text:sub(start, stop) + local h = hash(s) + PD.blocks[h] = s + text = text:sub(1,start) .. h .. text:sub(stop) + return text +end + +-- Protect every part of the text that matches any of the patterns. The first +-- matching pattern is protected first, etc. +local function protect_matches(text, patterns) + while true do + local start, stop = find_first(text, patterns) + if not start then break end + text = protect_range(text, start, stop) + end + return text +end + +-- Protects blocklevel tags in the specified text +local function protect(text) + -- First protect potentially nested block tags + text = protect_matches(text, map(PD.tags, block_pattern)) + -- Then protect block tags at the line level. + text = protect_matches(text, map(PD.tags, line_pattern)) + -- Protect
    and comment tags + text = protect_matches(text, {"\n]->[ \t]*\n"}) + text = protect_matches(text, {"\n[ \t]*\n"}) + return text +end + +-- Returns true if the string s is a hash resulting from protection +local function is_protected(s) + return PD.blocks[s] +end + +-- Unprotects the specified text by expanding all the nonces +local function unprotect(text) + for k,v in pairs(PD.blocks) do + v = v:gsub("%%", "%%%%") + text = text:gsub(k, v) + end + return text +end + + +---------------------------------------------------------------------- +-- Block transform +---------------------------------------------------------------------- + +-- The block transform functions transform the text on the block level. +-- They work with the text as an array of lines rather than as individual +-- characters. + +-- Returns true if the line is a ruler of (char) characters. +-- The line must contain at least three char characters and contain only spaces and +-- char characters. +local function is_ruler_of(line, char) + if not line:match("^[ %" .. char .. "]*$") then return false end + if not line:match("%" .. char .. ".*%" .. char .. ".*%" .. char) then return false end + return true +end + +-- Identifies the block level formatting present in the line +local function classify(line) + local info = {line = line, text = line} + + if line:match("^ ") then + info.type = "indented" + info.outdented = line:sub(5) + return info + end + + for _,c in ipairs({'*', '-', '_', '='}) do + if is_ruler_of(line, c) then + info.type = "ruler" + info.ruler_char = c + return info + end + end + + if line == "" then + info.type = "blank" + return info + end + + if line:match("^(#+)[ \t]*(.-)[ \t]*#*[ \t]*$") then + local m1, m2 = line:match("^(#+)[ \t]*(.-)[ \t]*#*[ \t]*$") + info.type = "header" + info.level = m1:len() + info.text = m2 + return info + end + + if line:match("^ ? ? ?(%d+)%.[ \t]+(.+)") then + local number, text = line:match("^ ? ? ?(%d+)%.[ \t]+(.+)") + info.type = "list_item" + info.list_type = "numeric" + info.number = 0 + number + info.text = text + return info + end + + if line:match("^ ? ? ?([%*%+%-])[ \t]+(.+)") then + local bullet, text = line:match("^ ? ? ?([%*%+%-])[ \t]+(.+)") + info.type = "list_item" + info.list_type = "bullet" + info.bullet = bullet + info.text= text + return info + end + + if line:match("^>[ \t]?(.*)") then + info.type = "blockquote" + info.text = line:match("^>[ \t]?(.*)") + return info + end + + if is_protected(line) then + info.type = "raw" + info.html = unprotect(line) + return info + end + + info.type = "normal" + return info +end + +-- Find headers constisting of a normal line followed by a ruler and converts them to +-- header entries. +local function headers(array) + local i = 1 + while i <= #array - 1 do + if array[i].type == "normal" and array[i+1].type == "ruler" and + (array[i+1].ruler_char == "-" or array[i+1].ruler_char == "=") then + local info = {line = array[i].line} + info.text = info.line + info.type = "header" + info.level = iff(array[i+1].ruler_char == "=", 1, 2) + table.remove(array, i+1) + array[i] = info + end + i = i + 1 + end + return array +end + +local block_transform, blocks_to_html, encode_code, span_transform, encode_backslash_escapes + +-- Find list blocks and convert them to protected data blocks +local function lists(array, sublist) + local function process_list(arr) + local function any_blanks(arr) + for i = 1, #arr do + if arr[i].type == "blank" then return true end + end + return false + end + + local function split_list_items(arr) + local acc = {arr[1]} + local res = {} + for i=2,#arr do + if arr[i].type == "list_item" then + table.insert(res, acc) + acc = {arr[i]} + else + table.insert(acc, arr[i]) + end + end + table.insert(res, acc) + return res + end + + local function process_list_item(lines, block) + while lines[#lines].type == "blank" do + table.remove(lines) + end + + local itemtext = lines[1].text + for i=2,#lines do + itemtext = itemtext .. "\n" .. outdent(lines[i].line) + end + if block then + itemtext = block_transform(itemtext, true) + if not itemtext:find("
    ") then itemtext = indent(itemtext) end
    +				return "    
  • " .. itemtext .. "
  • " + else + local lines = split(itemtext) + lines = map(lines, classify) + lines = lists(lines, true) + lines = blocks_to_html(lines, true) + itemtext = table.concat(lines, "\n") + if not itemtext:find("
    ") then itemtext = indent(itemtext) end
    +				return "    
  • " .. itemtext .. "
  • " + end + end + + local block_list = any_blanks(arr) + local items = split_list_items(arr) + local out = "" + for _, item in ipairs(items) do + out = out .. process_list_item(item, block_list) .. "\n" + end + if arr[1].list_type == "numeric" then + return "
      \n" .. out .. "
    " + else + return "
      \n" .. out .. "
    " + end + end + + -- Finds the range of lines composing the first list in the array. A list + -- starts with (^ list_item) or (blank list_item) and ends with + -- (blank* $) or (blank normal). + -- + -- A sublist can start with just (list_item) does not need a blank... + local function find_list(array, sublist) + local function find_list_start(array, sublist) + if array[1].type == "list_item" then return 1 end + if sublist then + for i = 1,#array do + if array[i].type == "list_item" then return i end + end + else + for i = 1, #array-1 do + if array[i].type == "blank" and array[i+1].type == "list_item" then + return i+1 + end + end + end + return nil + end + local function find_list_end(array, start) + local pos = #array + for i = start, #array-1 do + if array[i].type == "blank" and array[i+1].type ~= "list_item" + and array[i+1].type ~= "indented" and array[i+1].type ~= "blank" then + pos = i-1 + break + end + end + while pos > start and array[pos].type == "blank" do + pos = pos - 1 + end + return pos + end + + local start = find_list_start(array, sublist) + if not start then return nil end + return start, find_list_end(array, start) + end + + while true do + local start, stop = find_list(array, sublist) + if not start then break end + local text = process_list(splice(array, start, stop)) + local info = { + line = text, + type = "raw", + html = text + } + array = splice(array, start, stop, {info}) + end + + -- Convert any remaining list items to normal + for _,line in ipairs(array) do + if line.type == "list_item" then line.type = "normal" end + end + + return array +end + +-- Find and convert blockquote markers. +local function blockquotes(lines) + local function find_blockquote(lines) + local start + for i,line in ipairs(lines) do + if line.type == "blockquote" then + start = i + break + end + end + if not start then return nil end + + local stop = #lines + for i = start+1, #lines do + if lines[i].type == "blank" or lines[i].type == "blockquote" then + elseif lines[i].type == "normal" then + if lines[i-1].type == "blank" then stop = i-1 break end + else + stop = i-1 break + end + end + while lines[stop].type == "blank" do stop = stop - 1 end + return start, stop + end + + local function process_blockquote(lines) + local raw = lines[1].text + for i = 2,#lines do + raw = raw .. "\n" .. lines[i].text + end + local bt = block_transform(raw) + if not bt:find("
    ") then bt = indent(bt) end
    +		return "
    \n " .. bt .. + "\n
    " + end + + while true do + local start, stop = find_blockquote(lines) + if not start then break end + local text = process_blockquote(splice(lines, start, stop)) + local info = { + line = text, + type = "raw", + html = text + } + lines = splice(lines, start, stop, {info}) + end + return lines +end + +-- Find and convert codeblocks. +local function codeblocks(lines) + local function find_codeblock(lines) + local start + for i,line in ipairs(lines) do + if line.type == "indented" then start = i break end + end + if not start then return nil end + + local stop = #lines + for i = start+1, #lines do + if lines[i].type ~= "indented" and lines[i].type ~= "blank" then + stop = i-1 + break + end + end + while lines[stop].type == "blank" do stop = stop - 1 end + return start, stop + end + + local function process_codeblock(lines) + local raw = detab(encode_code(outdent(lines[1].line))) + for i = 2,#lines do + raw = raw .. "\n" .. detab(encode_code(outdent(lines[i].line))) + end + return "
    " .. raw .. "\n
    " + end + + while true do + local start, stop = find_codeblock(lines) + if not start then break end + local text = process_codeblock(splice(lines, start, stop)) + local info = { + line = text, + type = "raw", + html = text + } + lines = splice(lines, start, stop, {info}) + end + return lines +end + +-- Convert lines to html code +function blocks_to_html(lines, no_paragraphs) + local out = {} + local i = 1 + while i <= #lines do + local line = lines[i] + if line.type == "ruler" then + table.insert(out, "
    ") + elseif line.type == "raw" then + table.insert(out, line.html) + elseif line.type == "normal" then + local s = line.line + + while i+1 <= #lines and lines[i+1].type == "normal" do + i = i + 1 + s = s .. "\n" .. lines[i].line + end + + if no_paragraphs then + table.insert(out, span_transform(s)) + else + table.insert(out, "

    " .. span_transform(s) .. "

    ") + end + elseif line.type == "header" then + local s = "" .. span_transform(line.text) .. "" + table.insert(out, s) + else + table.insert(out, line.line) + end + i = i + 1 + end + return out +end + +-- Perform all the block level transforms +function block_transform(text, sublist) + local lines = split(text) + lines = map(lines, classify) + lines = headers(lines) + lines = lists(lines, sublist) + lines = codeblocks(lines) + lines = blockquotes(lines) + lines = blocks_to_html(lines) + local text = table.concat(lines, "\n") + return text +end + +-- Debug function for printing a line array to see the result +-- of partial transforms. +local function print_lines(lines) + for i, line in ipairs(lines) do + print(i, line.type, line.text or line.line) + end +end + +---------------------------------------------------------------------- +-- Span transform +---------------------------------------------------------------------- + +-- Functions for transforming the text at the span level. + +-- These characters may need to be escaped because they have a special +-- meaning in markdown. +local escape_chars = "'\\`*_{}[]()>#+-.!'" +local escape_table = {} + +local function init_escape_table() + escape_table = {} + for i = 1,#escape_chars do + local c = escape_chars:sub(i,i) + escape_table[c] = hash(c) + end +end + +-- Adds a new escape to the escape table. +local function add_escape(text) + if not escape_table[text] then + escape_table[text] = hash(text) + end + return escape_table[text] +end + +-- Escape characters that should not be disturbed by markdown. +local function escape_special_chars(text) + local tokens = tokenize_html(text) + + local out = "" + for _, token in ipairs(tokens) do + local t = token.text + if token.type == "tag" then + -- In tags, encode * and _ so they don't conflict with their use in markdown. + t = t:gsub("%*", escape_table["*"]) + t = t:gsub("%_", escape_table["_"]) + else + t = encode_backslash_escapes(t) + end + out = out .. t + end + return out +end + +-- Encode backspace-escaped characters in the markdown source. +function encode_backslash_escapes(t) + for i=1,escape_chars:len() do + local c = escape_chars:sub(i,i) + t = t:gsub("\\%" .. c, escape_table[c]) + end + return t +end + +-- Unescape characters that have been encoded. +local function unescape_special_chars(t) + local tin = t + for k,v in pairs(escape_table) do + k = k:gsub("%%", "%%%%") + t = t:gsub(v,k) + end + if t ~= tin then t = unescape_special_chars(t) end + return t +end + +-- Encode/escape certain characters inside Markdown code runs. +-- The point is that in code, these characters are literals, +-- and lose their special Markdown meanings. +function encode_code(s) + s = s:gsub("%&", "&") + s = s:gsub("<", "<") + s = s:gsub(">", ">") + for k,v in pairs(escape_table) do + s = s:gsub("%"..k, v) + end + return s +end + +-- Handle backtick blocks. +local function code_spans(s) + s = s:gsub("\\\\", escape_table["\\"]) + s = s:gsub("\\`", escape_table["`"]) + + local pos = 1 + while true do + local start, stop = s:find("`+", pos) + if not start then return s end + local count = stop - start + 1 + -- Find a matching numbert of backticks + local estart, estop = s:find(string.rep("`", count), stop+1) + local brstart = s:find("\n", stop+1) + if estart and (not brstart or estart < brstart) then + local code = s:sub(stop+1, estart-1) + code = code:gsub("^[ \t]+", "") + code = code:gsub("[ \t]+$", "") + code = code:gsub(escape_table["\\"], escape_table["\\"] .. escape_table["\\"]) + code = code:gsub(escape_table["`"], escape_table["\\"] .. escape_table["`"]) + code = "" .. encode_code(code) .. "" + code = add_escape(code) + s = s:sub(1, start-1) .. code .. s:sub(estop+1) + pos = start + code:len() + else + pos = stop + 1 + end + end + return s +end + +-- Encode alt text... enodes &, and ". +local function encode_alt(s) + if not s then return s end + s = s:gsub('&', '&') + s = s:gsub('"', '"') + s = s:gsub('<', '<') + return s +end + +local link_database + +-- Handle image references +local function images(text) + local function reference_link(alt, id) + alt = encode_alt(alt:match("%b[]"):sub(2,-2)) + id = id:match("%[(.*)%]"):lower() + if id == "" then id = text:lower() end + link_database[id] = link_database[id] or {} + if not link_database[id].url then return nil end + local url = link_database[id].url or id + url = encode_alt(url) + local title = encode_alt(link_database[id].title) + if title then title = " title=\"" .. title .. "\"" else title = "" end + return add_escape ('' .. alt .. '") + end + + local function inline_link(alt, link) + alt = encode_alt(alt:match("%b[]"):sub(2,-2)) + local url, title = link:match("%(?[ \t]*['\"](.+)['\"]") + url = url or link:match("%(?%)") + url = encode_alt(url) + title = encode_alt(title) + if title then + return add_escape('' .. alt .. '') + else + return add_escape('' .. alt .. '') + end + end + + text = text:gsub("!(%b[])[ \t]*\n?[ \t]*(%b[])", reference_link) + text = text:gsub("!(%b[])(%b())", inline_link) + return text +end + +-- Handle anchor references +local function anchors(text) + local function reference_link(text, id) + text = text:match("%b[]"):sub(2,-2) + id = id:match("%b[]"):sub(2,-2):lower() + if id == "" then id = text:lower() end + link_database[id] = link_database[id] or {} + if not link_database[id].url then return nil end + local url = link_database[id].url or id + url = encode_alt(url) + local title = encode_alt(link_database[id].title) + if title then title = " title=\"" .. title .. "\"" else title = "" end + return add_escape("") .. text .. add_escape("") + end + + local function inline_link(text, link) + text = text:match("%b[]"):sub(2,-2) + local url, title = link:match("%(?[ \t]*['\"](.+)['\"]") + title = encode_alt(title) + url = url or link:match("%(?%)") or "" + url = encode_alt(url) + if title then + return add_escape("") .. text .. "" + else + return add_escape("") .. text .. add_escape("") + end + end + + text = text:gsub("(%b[])[ \t]*\n?[ \t]*(%b[])", reference_link) + text = text:gsub("(%b[])(%b())", inline_link) + return text +end + +-- Handle auto links, i.e. . +local function auto_links(text) + local function link(s) + return add_escape("") .. s .. "" + end + -- Encode chars as a mix of dec and hex entitites to (perhaps) fool + -- spambots. + local function encode_email_address(s) + -- Use a deterministic encoding to make unit testing possible. + -- Code 45% hex, 45% dec, 10% plain. + local hex = {code = function(c) return "&#x" .. string.format("%x", c:byte()) .. ";" end, count = 1, rate = 0.45} + local dec = {code = function(c) return "&#" .. c:byte() .. ";" end, count = 0, rate = 0.45} + local plain = {code = function(c) return c end, count = 0, rate = 0.1} + local codes = {hex, dec, plain} + local function swap(t,k1,k2) local temp = t[k2] t[k2] = t[k1] t[k1] = temp end + + local out = "" + for i = 1,s:len() do + for _,code in ipairs(codes) do code.count = code.count + code.rate end + if codes[1].count < codes[2].count then swap(codes,1,2) end + if codes[2].count < codes[3].count then swap(codes,2,3) end + if codes[1].count < codes[2].count then swap(codes,1,2) end + + local code = codes[1] + local c = s:sub(i,i) + -- Force encoding of "@" to make email address more invisible. + if c == "@" and code == plain then code = codes[2] end + out = out .. code.code(c) + code.count = code.count - 1 + end + return out + end + local function mail(s) + s = unescape_special_chars(s) + local address = encode_email_address("mailto:" .. s) + local text = encode_email_address(s) + return add_escape("") .. text .. "" + end + -- links + text = text:gsub("<(https?:[^'\">%s]+)>", link) + text = text:gsub("<(ftp:[^'\">%s]+)>", link) + + -- mail + text = text:gsub("%s]+)>", mail) + text = text:gsub("<([-.%w]+%@[-.%w]+)>", mail) + return text +end + +-- Encode free standing amps (&) and angles (<)... note that this does not +-- encode free >. +local function amps_and_angles(s) + -- encode amps not part of &..; expression + local pos = 1 + while true do + local amp = s:find("&", pos) + if not amp then break end + local semi = s:find(";", amp+1) + local stop = s:find("[ \t\n&]", amp+1) + if not semi or (stop and stop < semi) or (semi - amp) > 15 then + s = s:sub(1,amp-1) .. "&" .. s:sub(amp+1) + pos = amp+1 + else + pos = amp+1 + end + end + + -- encode naked <'s + s = s:gsub("<([^a-zA-Z/?$!])", "<%1") + s = s:gsub("<$", "<") + + -- what about >, nothing done in the original markdown source to handle them + return s +end + +-- Handles emphasis markers (* and _) in the text. +local function emphasis(text) + for _, s in ipairs {"%*%*", "%_%_"} do + text = text:gsub(s .. "([^%s][%*%_]?)" .. s, "%1") + text = text:gsub(s .. "([^%s][^<>]-[^%s][%*%_]?)" .. s, "%1") + end + for _, s in ipairs {"%*", "%_"} do + text = text:gsub(s .. "([^%s_])" .. s, "%1") + text = text:gsub(s .. "([^%s_])" .. s, "%1") + text = text:gsub(s .. "([^%s_][^<>_]-[^%s_])" .. s, "%1") + text = text:gsub(s .. "([^<>_]-[^<>_]-[^<>_]-)" .. s, "%1") + end + return text +end + +-- Handles line break markers in the text. +local function line_breaks(text) + return text:gsub(" +\n", "
    \n") +end + +-- Perform all span level transforms. +function span_transform(text) + text = code_spans(text) + text = escape_special_chars(text) + text = images(text) + text = anchors(text) + text = auto_links(text) + text = amps_and_angles(text) + text = emphasis(text) + text = line_breaks(text) + return text +end + +---------------------------------------------------------------------- +-- Markdown +---------------------------------------------------------------------- + +-- Cleanup the text by normalizing some possible variations to make further +-- processing easier. +local function cleanup(text) + -- Standardize line endings + text = text:gsub("\r\n", "\n") -- DOS to UNIX + text = text:gsub("\r", "\n") -- Mac to UNIX + + -- Convert all tabs to spaces + text = detab(text) + + -- Strip lines with only spaces and tabs + while true do + local subs + text, subs = text:gsub("\n[ \t]+\n", "\n\n") + if subs == 0 then break end + end + + return "\n" .. text .. "\n" +end + +-- Strips link definitions from the text and stores the data in a lookup table. +local function strip_link_definitions(text) + local linkdb = {} + + local function link_def(id, url, title) + id = id:match("%[(.+)%]"):lower() + linkdb[id] = linkdb[id] or {} + linkdb[id].url = url or linkdb[id].url + linkdb[id].title = title or linkdb[id].title + return "" + end + + local def_no_title = "\n ? ? ?(%b[]):[ \t]*\n?[ \t]*]+)>?[ \t]*" + local def_title1 = def_no_title .. "[ \t]+\n?[ \t]*[\"'(]([^\n]+)[\"')][ \t]*" + local def_title2 = def_no_title .. "[ \t]*\n[ \t]*[\"'(]([^\n]+)[\"')][ \t]*" + local def_title3 = def_no_title .. "[ \t]*\n?[ \t]+[\"'(]([^\n]+)[\"')][ \t]*" + + text = text:gsub(def_title1, link_def) + text = text:gsub(def_title2, link_def) + text = text:gsub(def_title3, link_def) + text = text:gsub(def_no_title, link_def) + return text, linkdb +end + +link_database = {} + +-- Main markdown processing function +local function markdown(text) + init_hash(text) + init_escape_table() + + text = cleanup(text) + text = protect(text) + text, link_database = strip_link_definitions(text) + text = block_transform(text) + text = unescape_special_chars(text) + return text +end + +---------------------------------------------------------------------- +-- End of module +---------------------------------------------------------------------- + +M.lock(M) + +-- Expose markdown function to the world +_G.markdown = M.markdown + +-- Class for parsing command-line options +local OptionParser = {} +OptionParser.__index = OptionParser + +-- Creates a new option parser +function OptionParser:new() + local o = {short = {}, long = {}} + setmetatable(o, self) + return o +end + +-- Calls f() whenever a flag with specified short and long name is encountered +function OptionParser:flag(short, long, f) + local info = {type = "flag", f = f} + if short then self.short[short] = info end + if long then self.long[long] = info end +end + +-- Calls f(param) whenever a parameter flag with specified short and long name is encountered +function OptionParser:param(short, long, f) + local info = {type = "param", f = f} + if short then self.short[short] = info end + if long then self.long[long] = info end +end + +-- Calls f(v) for each non-flag argument +function OptionParser:arg(f) + self.arg = f +end + +-- Runs the option parser for the specified set of arguments. Returns true if all arguments +-- where successfully parsed and false otherwise. +function OptionParser:run(args) + local pos = 1 + local param + while pos <= #args do + local arg = args[pos] + if arg == "--" then + for i=pos+1,#args do + if self.arg then self.arg(args[i]) end + return true + end + end + if arg:match("^%-%-") then + local info = self.long[arg:sub(3)] + if not info then print("Unknown flag: " .. arg) return false end + if info.type == "flag" then + info.f() + pos = pos + 1 + else + param = args[pos+1] + if not param then print("No parameter for flag: " .. arg) return false end + info.f(param) + pos = pos+2 + end + elseif arg:match("^%-") then + for i=2,arg:len() do + local c = arg:sub(i,i) + local info = self.short[c] + if not info then print("Unknown flag: -" .. c) return false end + if info.type == "flag" then + info.f() + else + if i == arg:len() then + param = args[pos+1] + if not param then print("No parameter for flag: -" .. c) return false end + info.f(param) + pos = pos + 1 + else + param = arg:sub(i+1) + info.f(param) + end + break + end + end + pos = pos + 1 + else + if self.arg then self.arg(arg) end + pos = pos + 1 + end + end + return true +end + +-- Handles the case when markdown is run from the command line +local function run_command_line(arg) + -- Generate output for input s given options + local function run(s, options) + s = markdown(s) + if not options.wrap_header then return s end + local header = "" + if options.header then + local f = io.open(options.header) or error("Could not open file: " .. options.header) + header = f:read("*a") + f:close() + else + header = [[ + + + + + TITLE + + + +]] + local title = options.title or s:match("

    (.-)

    ") or s:match("

    (.-)

    ") or + s:match("

    (.-)

    ") or "Untitled" + header = header:gsub("TITLE", title) + if options.inline_style then + local style = "" + local f = io.open(options.stylesheet) + if f then + style = f:read("*a") f:close() + else + error("Could not include style sheet " .. options.stylesheet .. ": File not found") + end + header = header:gsub('', + "") + else + header = header:gsub("STYLESHEET", options.stylesheet) + end + header = header:gsub("CHARSET", options.charset) + end + local footer = "" + if options.footer then + local f = io.open(options.footer) or error("Could not open file: " .. options.footer) + footer = f:read("*a") + f:close() + end + return header .. s .. footer + end + + -- Generate output path name from input path name given options. + local function outpath(path, options) + if options.append then return path .. ".html" end + local m = path:match("^(.+%.html)[^/\\]+$") if m then return m end + m = path:match("^(.+%.)[^/\\]*$") if m and path ~= m .. "html" then return m .. "html" end + return path .. ".html" + end + + -- Default commandline options + local options = { + wrap_header = true, + header = nil, + footer = nil, + charset = "utf-8", + title = nil, + stylesheet = "default.css", + inline_style = false + } + local help = [[ +Usage: markdown.lua [OPTION] [FILE] +Runs the markdown text markup to HTML converter on each file specified on the +command line. If no files are specified, runs on standard input. + +No header: + -n, --no-wrap Don't wrap the output in ... tags. +Custom header: + -e, --header FILE Use content of FILE for header. + -f, --footer FILE Use content of FILE for footer. +Generated header: + -c, --charset SET Specifies charset (default utf-8). + -i, --title TITLE Specifies title (default from first

    tag). + -s, --style STYLE Specifies style sheet file (default default.css). + -l, --inline-style Include the style sheet file inline in the header. +Generated files: + -a, --append Append .html extension (instead of replacing). +Other options: + -h, --help Print this help text. + -t, --test Run the unit tests. +]] + + local run_stdin = true + local op = OptionParser:new() + op:flag("n", "no-wrap", function () options.wrap_header = false end) + op:param("e", "header", function (x) options.header = x end) + op:param("f", "footer", function (x) options.footer = x end) + op:param("c", "charset", function (x) options.charset = x end) + op:param("i", "title", function(x) options.title = x end) + op:param("s", "style", function(x) options.stylesheet = x end) + op:flag("l", "inline-style", function(x) options.inline_style = true end) + op:flag("a", "append", function() options.append = true end) + op:flag("t", "test", function() + local n = arg[0]:gsub("markdown.lua", "markdown-tests.lua") + local f = io.open(n) + if f then + f:close() dofile(n) + else + error("Cannot find markdown-tests.lua") + end + run_stdin = false + end) + op:flag("h", "help", function() print(help) run_stdin = false end) + op:arg(function(path) + local file = io.open(path) or error("Could not open file: " .. path) + local s = file:read("*a") + file:close() + s = run(s, options) + file = io.open(outpath(path, options), "w") or error("Could not open output file: " .. outpath(path, options)) + file:write(s) + file:close() + run_stdin = false + end + ) + + if not op:run(arg) then + print(help) + run_stdin = false + end + + if run_stdin then + local s = io.read("*a") + s = run(s, options) + io.write(s) + end +end + +-- If we are being run from the command-line, act accordingly +if arg and arg[0]:find("markdown%.lua$") then + run_command_line(arg) +else + return markdown +end diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/samples/toycms/populate_mysql_blog.lua lua-orbit-2.2.1+dfsg/samples/toycms/populate_mysql_blog.lua --- lua-orbit-2.2.0+gita6fb46e+dfsg/samples/toycms/populate_mysql_blog.lua 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/samples/toycms/populate_mysql_blog.lua 2014-02-02 09:25:01.000000000 +0000 @@ -1,12 +1,12 @@ local db = 'blog' -require "luasql.mysql" -require "orbit.model" +local luasql = require "luasql.mysql" +local orm = require "orbit.model" -local env = luasql.mysql() +local env = luasql() local conn = env:connect(db, "root", "password") -local mapper = orbit.model.new("toycms_", conn, "mysql") +local mapper = orm.new("toycms_", conn, "mysql") diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/samples/toycms/populate_mysql_lablua.lua lua-orbit-2.2.1+dfsg/samples/toycms/populate_mysql_lablua.lua --- lua-orbit-2.2.0+gita6fb46e+dfsg/samples/toycms/populate_mysql_lablua.lua 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/samples/toycms/populate_mysql_lablua.lua 2014-02-02 09:25:01.000000000 +0000 @@ -1,14 +1,12 @@ local db = 'lablua' -require "luasql.mysql" -require "orbit.model" +local luasql = require "luasql.mysql" +local orm = require "orbit.model" -local env = luasql.mysql() +local env = luasql() local conn = env:connect(db, "root", "password") -local mapper = orbit.model.new("toycms_", conn, "mysql") - - +local mapper = orm.new("toycms_", conn, "mysql") -- Table post diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/samples/toycms/toycms.cgi lua-orbit-2.2.1+dfsg/samples/toycms/toycms.cgi --- lua-orbit-2.2.0+gita6fb46e+dfsg/samples/toycms/toycms.cgi 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/samples/toycms/toycms.cgi 2014-02-02 09:25:01.000000000 +0000 @@ -1,12 +1,12 @@ #!/usr/bin/env lua51 -require"lfs" +local lfs = require "lfs" lfs.chdir("/path/to/toycms") -require"wsapi.cgi" +local wscgi = require "wsapi.cgi" -require"toycms" +local toycms = require "toycms" -wsapi.cgi.run(toycms.run) +wscgi.run(toycms.run) diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/samples/toycms/toycms.fcgi lua-orbit-2.2.1+dfsg/samples/toycms/toycms.fcgi --- lua-orbit-2.2.0+gita6fb46e+dfsg/samples/toycms/toycms.fcgi 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/samples/toycms/toycms.fcgi 2014-02-02 09:25:01.000000000 +0000 @@ -1,11 +1,11 @@ #!/usr/bin/env lua51 -require"lfs" +local lfs = require "lfs" lfs.chdir("/path/to/toycms") -require"wsapi.fastcgi" +local wsfcgi = require "wsapi.fastcgi" -require"toycms" +local toycms = require"toycms" -wsapi.fastcgi.run(toycms.run) +wsfcgi.run(toycms.run) diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/samples/toycms/toycms.lua lua-orbit-2.2.1+dfsg/samples/toycms/toycms.lua --- lua-orbit-2.2.0+gita6fb46e+dfsg/samples/toycms/toycms.lua 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/samples/toycms/toycms.lua 2014-02-02 09:25:01.000000000 +0000 @@ -1,19 +1,25 @@ #!/usr/bin/env wsapi.cgi -require "orbit" -require "markdown" -require "orbit.cache" -require "cosmo" - -module("toycms", package.seeall, orbit.new) +local orbit = require "orbit" +local markdown = require "markdown" +local orcache = require "orbit.cache" +local cosmo = require "cosmo" +local wsutil = require "wsapi.util" + +local toycms = setmetatable(orbit.new(), { __index = _G }) +if _VERSION == "Lua 5.2" then + _ENV = toycms +else + setfenv(1, toycms) +end plugins = {} -require "toycms_config" -require "toycms_plugins" -require "toycms_admin" +wsutil.loadfile("toycms_config.lua", toycms)() +wsutil.loadfile("toycms_plugins.lua", toycms)() +wsutil.loadfile("toycms_admin.lua", toycms)(toycms) -require("luasql." .. database.driver) +local luasql = require("luasql." .. database.driver) local env = luasql[database.driver]() mapper.conn = env:connect(unpack(database.conn_data)) mapper.driver = database.driver @@ -25,7 +31,7 @@ user = toycms:model "user" } -cache = orbit.cache.new(toycms, cache_path) +cache = orcache.new(toycms, cache_path) function models.post:find_comments() return models.comment:find_all_by_approved_and_post_id{ true, @@ -68,7 +74,7 @@ function load_template(name) local template = template_cache[name] if not template then - local template_file = io.open(toycms.real_path .. "/templates/" .. + local template_file = io.open(real_path .. "/templates/" .. template_name .. "/" .. name, "rb") if template_file then template = cosmo.compile(template_file:read("*a")) @@ -170,7 +176,7 @@ if not has_block then local out = {} local template = load_template((arg and arg.template) or - "comment.html") + "comment.html") end for _, comment in ipairs(comments) do if has_block then @@ -262,8 +268,8 @@ if not post then return not_found(web) end local section = models.section:find(post.section_id) local template = load_template("post_" .. - tostring(section.tag) .. - "." .. type) or + tostring(section.tag) .. + "." .. type) or load_template("post." .. type) if template then web.input.section_id = post.section_id @@ -330,10 +336,9 @@ post.n_comments = (post.n_comments or 0) + 1 post:save() cache:invalidate("/", "/xml", "/section/" .. post.section_id, - "/section/" .. post.section_id .. "/xml", - "/post/" .. post.id, "/post/" .. post.id .. "/xml", - "/archive/" .. - os.date("%Y/%m", post.published_at)) + "/section/" .. post.section_id .. "/xml", + "/post/" .. post.id, "/post/" .. post.id .. "/xml", + "/archive/" .. os.date("%Y/%m", post.published_at)) else comment.approved = false end comment:save() return web:redirect(web:link("/post/" .. post_id)) @@ -362,4 +367,5 @@ return models.user:find_by_id_and_password{ user_id, password } end -return _M +return toycms + diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/samples/toycms/toycms_admin.lua lua-orbit-2.2.1+dfsg/samples/toycms/toycms_admin.lua --- lua-orbit-2.2.0+gita6fb46e+dfsg/samples/toycms/toycms_admin.lua 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/samples/toycms/toycms_admin.lua 2014-02-02 09:25:01.000000000 +0000 @@ -1,5 +1,6 @@ -module("toycms", package.seeall) +local orbit = require "orbit" +local toycms = ... -- Admin interface @@ -644,4 +645,5 @@ } end -orbit.htmlify(toycms, "_.+", "admin_layout", "render_.+") \ No newline at end of file +orbit.htmlify(toycms, "_.+", "admin_layout", "render_.+") + diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/samples/toycms/toycms_config.lua lua-orbit-2.2.1+dfsg/samples/toycms/toycms_config.lua --- lua-orbit-2.2.0+gita6fb46e+dfsg/samples/toycms/toycms_config.lua 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/samples/toycms/toycms_config.lua 2014-02-02 09:25:01.000000000 +0000 @@ -1,11 +1,10 @@ -module("toycms", package.seeall) -- Uncomment next line to enable X-Sendfile for sending static files -- use_xsendfile = true database = { driver = "sqlite3", - conn_data = { toycms.real_path .. "/blog.db" } + conn_data = { real_path .. "/blog.db" } -- driver = "mysql", -- conn_data = { "blog", "root", "password" } } @@ -13,7 +12,7 @@ template_name = "blog" -- Comment this for in-memory caching -cache_path = toycms.real_path .. "/page_cache" +cache_path = real_path .. "/page_cache" -- Uncomment the following line to set a url prefix -- prefix = "/foobar" diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/samples/toycms/toycms_plugins.lua lua-orbit-2.2.1+dfsg/samples/toycms/toycms_plugins.lua --- lua-orbit-2.2.0+gita6fb46e+dfsg/samples/toycms/toycms_plugins.lua 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/samples/toycms/toycms_plugins.lua 2014-02-02 09:25:01.000000000 +0000 @@ -1,4 +1,3 @@ -module("toycms", package.seeall) function plugins.date(web) return { @@ -154,3 +153,4 @@ end } end + diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/src/launchers/op.cgi lua-orbit-2.2.1+dfsg/src/launchers/op.cgi --- lua-orbit-2.2.0+gita6fb46e+dfsg/src/launchers/op.cgi 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/src/launchers/op.cgi 2014-02-02 09:25:01.000000000 +0000 @@ -3,8 +3,6 @@ -- Orbit pages launcher, extracts script to launch -- from SCRIPT_FILENAME/PATH_TRANSLATED -pcall(require, "luarocks.require") - local common = require "wsapi.common" local cgi = require "wsapi.cgi" diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/src/launchers/op.fcgi lua-orbit-2.2.1+dfsg/src/launchers/op.fcgi --- lua-orbit-2.2.0+gita6fb46e+dfsg/src/launchers/op.fcgi 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/src/launchers/op.fcgi 2014-02-02 09:25:01.000000000 +0000 @@ -2,8 +2,6 @@ -- Orbit pages launcher, extracts script to launch -pcall(require, "luarocks.require") - local common = require "wsapi.common" local ok, err = pcall(require, "wsapi.fastcgi") diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/src/launchers/orbit lua-orbit-2.2.1+dfsg/src/launchers/orbit --- lua-orbit-2.2.0+gita6fb46e+dfsg/src/launchers/orbit 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/src/launchers/orbit 2014-02-02 09:25:01.000000000 +0000 @@ -7,20 +7,20 @@ -- ------------------------------------------------------------------------------- -pcall(require, "luarocks.require") - local ok, err = pcall(require, "wsapi.xavante") if not ok then io.stderr:write("WSAPI Xavante support not loaded:\n" .. err .. "\n\nPlease install wsapi-xavante with LuaRocks\n") os.exit(1) end -require "lfs" -require "xavante" -require "xavante.httpd" -require "xavante.vhostshandler" -require "xavante.urlhandler" -require "wsapi.util" +local lfs = require "lfs" +local xavante = require "xavante" +local httpd = require "xavante.httpd" +local vhostshandler = require "xavante.vhostshandler" +local urlhandler = require "xavante.urlhandler" +local util = require "wsapi.util" +local wsapi = require "wsapi" +local wsx = require "wsapi.xavante" local usage = [[ Usage: orbit [options] @@ -37,7 +37,7 @@ ]] -local opts, args = wsapi.util.getopt({ ... }, "lp") +local opts, args = util.getopt({ ... }, "lp") local app, app_args = args[1], { select(2, unpack(args)) } @@ -90,13 +90,13 @@ if type(app) == "table" then app = app.run end -xavante.httpd.handle_request = xavante.vhostshandler { - [""] = xavante.urlhandler { - ["/"] = wsapi.xavante.makeHandler (app, nil, path, path), +httpd.handle_request = vhostshandler { + [""] = urlhandler { + ["/"] = wsx.makeHandler (app, nil, path, path), } } -xavante.httpd.register ("*", tonumber(opts.p or opts.port) or 8080, "Xavante 2.0") +httpd.register ("*", tonumber(opts.p or opts.port) or 8080, "Xavante 2.3") xavante.start_message(function (ports) print(string.format("Starting Orbit server at port %s", diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/src/orbit/cache.lua lua-orbit-2.2.1+dfsg/src/orbit/cache.lua --- lua-orbit-2.2.0+gita6fb46e+dfsg/src/orbit/cache.lua 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/src/orbit/cache.lua 2014-02-02 09:25:01.000000000 +0000 @@ -1,5 +1,4 @@ - -require "lfs" +local lfs = require "lfs" module("orbit.cache", package.seeall) diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/src/orbit/model.lua lua-orbit-2.2.1+dfsg/src/orbit/model.lua --- lua-orbit-2.2.0+gita6fb46e+dfsg/src/orbit/model.lua 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/src/orbit/model.lua 2014-02-02 09:25:01.000000000 +0000 @@ -1,6 +1,5 @@ - -require "lpeg" -require "re" +local lpeg = require "lpeg" +local re = require "re" module("orbit.model", package.seeall) @@ -36,6 +35,10 @@ local convert = {} +function convert.real(v) + return tonumber(v) +end + function convert.float(v) return tonumber(v) end @@ -108,6 +111,10 @@ local escape = {} +function escape.real(v) + return tostring(v) +end + function escape.float(v) return tostring(v) end @@ -201,26 +208,35 @@ return rows end +local by_condition_parser = re.compile([[ + fields <- ({(!conective .)+} (conective {(!conective .)+})*) -> {} + conective <- and / or + and <- "_and_" -> "and" + or <- "_or_" -> "or" +]]) + local function parse_condition(dao, condition, args) - condition = string.gsub(condition, "_and_", "|") - local pairs = {} - for field in string.gmatch(condition, "[_%w]+") do - local i = #pairs + 1 - local value - if args[i] == nil then - pairs[i] = field .. " is null" - elseif type(args[i]) == "table" then - local values = {} - for _, value in ipairs(args[i]) do - values[#values + 1] = escape[dao.meta[field].type](value, dao.driver, dao.model.conn) + local parts = by_condition_parser:match(condition) + local j = 0 + for i, part in ipairs(parts) do + if part ~= "or" and part ~= "and" then + j = j + 1 + local value + if args[j] == nil then + parts[i] = part .. " is null" + elseif type(args[j]) == "table" then + local values = {} + for _, value in ipairs(args[j]) do + values[#values + 1] = escape[dao.meta[part].type](value, dao.driver, dao.model.conn) + end + parts[i] = part .. " IN (" .. table.concat(values,", ") .. ")" + else + value = escape[dao.meta[part].type](args[j], dao.driver, dao.model.conn) + parts[i] = part .. " = " .. value end - pairs[i] = field .. " IN (" .. table.concat(values,", ") .. ")" - else - value = escape[dao.meta[field].type](args[i], dao.driver, dao.model.conn) - pairs[i] = field .. " = " .. value end end - return pairs + return parts end local function build_inject(project, inject, dao) @@ -248,14 +264,15 @@ end local function build_query_by(dao, condition, args) - local pairs = parse_condition(dao, condition, args) + local parts = parse_condition(dao, condition, args) local order = "" local field_list, table_list, select, limit if args.distinct then select = "select distinct " else select = "select " end if tonumber(args.count) then limit = " limit " .. tonumber(args.count) else limit = "" end if args.order then order = " order by " .. args.order end if args.inject then - field_list, table_list, pairs[#pairs + 1] = build_inject(args.fields, args.inject, + if #parts > 0 then parts[#parts + 1] = "and" end + field_list, table_list, parts[#parts + 1] = build_inject(args.fields, args.inject, dao) else if args.fields then @@ -266,7 +283,7 @@ table_list = dao.table_name end local sql = select .. field_list .. " from " .. table_list .. - " where " .. table.concat(pairs, " and ") .. order .. limit + " where " .. table.concat(parts, " ") .. order .. limit if dao.model.logging then log_query(sql) end return sql end @@ -345,9 +362,9 @@ if not type(id) == "number" then error("find error: id must be a number") end - if dao.logging then log_query(sql) end local sql = "select * from " .. dao.table_name .. " where id=" .. id + if dao.logging then log_query(sql) end return fetch_row(dao, sql) end diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/src/orbit/ophandler.lua lua-orbit-2.2.1+dfsg/src/orbit/ophandler.lua --- lua-orbit-2.2.0+gita6fb46e+dfsg/src/orbit/ophandler.lua 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/src/orbit/ophandler.lua 2014-02-02 09:25:01.000000000 +0000 @@ -5,16 +5,17 @@ -- ----------------------------------------------------------------------------- -require "wsapi.xavante" -require "wsapi.common" - -module ("orbit.ophandler", package.seeall) +local wsapi = require "wsapi" +local wsxav = require "wsapi.xavante" +local wscom = require "wsapi.common" ------------------------------------------------------------------------------- -- Returns the Orbit Pages handler ------------------------------------------------------------------------------- -function makeHandler (diskpath, params) +local function makeHandler (diskpath, params) params = setmetatable({ modname = params.modname or "orbit.pages" }, { __index = params or {} }) - local op_loader = wsapi.common.make_isolated_launcher(params) - return wsapi.xavante.makeHandler(op_loader, nil, diskpath) + local op_loader = wscom.make_isolated_launcher(params) + return wsxav.makeHandler(op_loader, nil, diskpath) end + +return { makeHandler = makeHandler } diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/src/orbit/routes.lua lua-orbit-2.2.1+dfsg/src/orbit/routes.lua --- lua-orbit-2.2.0+gita6fb46e+dfsg/src/orbit/routes.lua 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/src/orbit/routes.lua 2014-02-02 09:25:01.000000000 +0000 @@ -1,3 +1,4 @@ +local setmetatable, type, ipairs, table, string = setmetatable, type, ipairs, table, string local lpeg = require "lpeg" local re = require "re" @@ -5,130 +6,184 @@ local _M = {} -local function foldr(t, f, acc) - for i = #t, 1, -1 do - acc = f(t[i], acc) - end - return acc -end - -local function foldl(t, f, acc) - for i = 1, #t do - acc = f(acc, t[i]) - end - return acc -end - -local param = re.compile[[ {[/%.]} ':' {[_%w]+} &('/' / {'.'} / !.) ]] / - function (prefix, name, dot) - local extra = { inner = (lpeg.P(1) - lpeg.S("/" .. (dot or "")))^1, - close = lpeg.P"/" + lpeg.P(dot or -1) + lpeg.P(-1) } - return { cap = lpeg.Carg(1) * re.compile([[ [/%.] {%inner+} &(%close) ]], extra) / - function (params, item, delim) - params[name] = util.url_decode(item) - end, - clean = re.compile([[ [/%.] %inner &(%close) ]], extra), - tag = "param", name = name, prefix = prefix } - end - -local opt_param = re.compile[[ {[/%.]} '?:' {[_%w]+} '?' &('/' / {'.'} / !.) ]] / - function (prefix, name, dot) - local extra = { inner = (lpeg.P(1) - lpeg.S("/" .. (dot or "")))^1, - close = lpeg.P"/" + lpeg.P(dot or -1) + lpeg.P(-1) } - return { cap = (lpeg.Carg(1) * re.compile([[ [/%.] {%inner+} &(%close) ]], extra) / - function (params, item, delim) - params[name] = util.url_decode(item) - end)^-1, - clean = re.compile([[ [/%.] %inner &(%close) ]], extra)^-1, - tag = "opt", name = name, prefix = prefix } - end - -local splat = re.compile[[ {[/%.]} {'*'} &('/' / '.' / !.) ]] / - function (prefix) - return { cap = "*", tag = "splat", prefix = prefix } - end - -local rest = lpeg.C((lpeg.P(1) - param - opt_param - splat)^1) +local alpha = lpeg.R('AZ', 'az') +local number = lpeg.R('09') +local asterisk = lpeg.P('*') +local question_mark = lpeg.P('?') +local at_sign = lpeg.P('@') +local colon = lpeg.P(':') +local the_dot = lpeg.P('.') +local underscore = lpeg.P('_') +local forward_slash = lpeg.P('/') +local slash_or_dot = forward_slash + the_dot + +local function cap_param(prefix, name, dot) + local inner = (1 - lpeg.S('/' .. (dot or '')))^1 + local close = lpeg.P'/' + (dot or -1) + -1 + return { + cap = lpeg.Carg(1) * slash_or_dot * lpeg.C(inner^1) * #close / function (params, item, delim) params[name] = util.url_decode(item) end, + clean = slash_or_dot * inner^1 * #close, + tag = "param", + name = name, + prefix = prefix + } +end + +local param_pre = lpeg.C(slash_or_dot) * colon * lpeg.C((alpha + number + underscore)^1) + +local param = (param_pre * #(forward_slash + -1) / cap_param) + + (param_pre * #the_dot / function (prefix, name) return cap_param(prefix, name, ".") end) + +local function cap_opt_param(prefix, name, dot) + local inner = (1 - lpeg.S('/' .. (dot or '')))^1 + local close = lpeg.P('/') + lpeg.P(dot or -1) + -1 + return { + cap = (lpeg.Carg(1) * slash_or_dot * lpeg.C(inner) * #close / function (params, item, delim) params[name] = util.url_decode(item) end)^-1, + clean = (slash_or_dot * inner * #lpeg.C(close))^-1, + tag = "opt", + name = name, + prefix = prefix + } +end + +local opt_param_pre = lpeg.C(slash_or_dot) * question_mark * colon * lpeg.C((alpha + number + underscore)^1) * question_mark + +local opt_param = (opt_param_pre * #(forward_slash + -1) / cap_opt_param) + + (opt_param_pre * #the_dot / function (prefix, name) return cap_opt_param(prefix, name, ".") end) + +local splat = lpeg.P(lpeg.C(forward_slash + the_dot) * asterisk * #(forward_slash + the_dot + -1)) / + function (prefix) + return { + cap = "*", + tag = "splat", + prefix = prefix + } + end + +local rest = lpeg.C((1 - param - opt_param - splat)^1) -local function fold_caps(cap, acc) +local function fold_captures(cap, acc) if type(cap) == "string" then - return { cap = lpeg.P(cap) * acc.cap, clean = lpeg.P(cap) * acc.clean } - elseif cap.cap == "*" then - return { cap = (lpeg.Carg(1) * (lpeg.P(cap.prefix) * lpeg.C((lpeg.P(1) - acc.clean)^0))^-1 / - function (params, splat) - if not params.splat then params.splat = {} end - if splat and splat ~= "" then - params.splat[#params.splat+1] = util.url_decode(splat) - end - end) * acc.cap, - clean = (lpeg.P(cap.prefix) * (lpeg.P(1) - acc.clean)^0)^-1 * acc.clean } - else - return { cap = cap.cap * acc.cap, clean = cap.clean * acc.clean } + return { + cap = lpeg.P(cap) * acc.cap, + clean = lpeg.P(cap) * acc.clean + } end + + -- if we have a star match (match everything) + if cap.cap == "*" then + return { + cap = (lpeg.Carg(1) * (cap.prefix * lpeg.C((1 - acc.clean)^0))^-1 / + function (params, splat) + params.splat = params.splat or {} + if splat and splat ~= "" then + params.splat[#params.splat+1] = util.url_decode(splat) + end + end) * acc.cap, + clean = (cap.prefix * (1 - acc.clean)^0)^-1 * acc.clean + } + end + + return { + cap = cap.cap * acc.cap, + clean = cap.clean * acc.clean + } end local function fold_parts(parts, cap) - if type(cap) == "string" then - parts[#parts+1] = { tag = "text", text = cap } - else - parts[#parts+1] = { tag = cap.tag, prefix = cap.prefix, name = cap.name } + + if type(cap) == "string" then -- if the capture is a string + parts[#parts+1] = { + tag = "text", + text = cap + } + else -- it must be a table capture + parts[#parts+1] = { + tag = cap.tag, + prefix = cap.prefix, + name = cap.name + } end + return parts end -local route = lpeg.Ct((param + opt_param + splat + rest)^1 * lpeg.P(-1)) / - function (caps) - return foldr(caps, fold_caps, { cap = lpeg.P("/")^-1 * lpeg.P(-1), clean = lpeg.P("/")^-1 * lpeg.P(-1) }), - foldl(caps, fold_parts, {}) - end +-- the right part (a bottom to top loop) +local function fold_right(t, f, acc) + for i = #t, 1, -1 do + acc = f(t[i], acc) + end + return acc +end + +-- the left part (a top to bottom loop) +local function fold_left(t, f, acc) + for i = 1, #t do + acc = f(acc, t[i]) + end + return acc +end + +local route = lpeg.Ct((param + opt_param + splat + rest)^0 * -1) / function (caps) + local forward_slash_at_end = lpeg.P('/')^-1 * -1 + return fold_right(caps, fold_captures, { cap = forward_slash_at_end, clean = forward_slash_at_end }), fold_left(caps, fold_parts, {}) +end local function build(parts, params) - local res = {} - local i = 1 + local res, i = {}, 1 params = params or {} params.splat = params.splat or {} for _, part in ipairs(parts) do - if part.tag == "param" then - if not params[part.name] then - error("route parameter " .. part.name .. " does not exist") - end - local s = string.gsub (params[part.name], "([^%.@]+)", - function (s) return util.url_encode(s) end) + if part.tag == 'param' then + if not params[part.name] then error('route parameter ' .. part.name .. ' does not exist') end + local s = string.gsub (params[part.name], '([^%.@]+)', function (s) return util.url_encode(s) end) res[#res+1] = part.prefix .. s elseif part.tag == "splat" then - local s = string.gsub (params.splat[i] or "", "([^/%.@]+)", - function (s) return util.url_encode(s) end) + local s = string.gsub (params.splat[i] or '', '([^/%.@]+)', function (s) return util.url_encode(s) end) res[#res+1] = part.prefix .. s i = i + 1 elseif part.tag == "opt" then if params and params[part.name] then - local s = string.gsub (params[part.name], "([^%.@]+)", - function (s) return util.url_encode(s) end) - res[#res+1] = part.prefix .. s + local s = string.gsub (params[part.name], '([^%.@]+)', function (s) return util.url_encode(s) end) + res[#res+1] = part.prefix .. s end else res[#res+1] = part.text end end - if #res > 0 then return table.concat(res) else return "/" end + + if #res > 0 then + return table.concat(res) + end + + return '/' end function _M.R(path) - local p, b = route:match(path) - return setmetatable({ parser = p.cap, parts = b }, - { __index = { - match = function (t, s) - local params = {} - if t.parser:match(s, 1, params) then - return params - else - return nil - end - end, - build = function (t, params) - return build(t.parts, params) - end - } }) + local p, b = route:match(path) + + return setmetatable({ + parser = p.cap, + parts = b + }, { + __index = { + match = function (t, s) + local params = {} + if t.parser:match(s, 1, params) then + return params + end + return nil + end, + build = function (t, params) + return build(t.parts, params) + end + } + }) + end -return setmetatable(_M, { __call = function (_, path) return _M.R(path) end }) +return setmetatable(_M, { + __call = function (_, path) + return _M.R(path) + end +}) diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/src/orbit.lua lua-orbit-2.2.1+dfsg/src/orbit.lua --- lua-orbit-2.2.0+gita6fb46e+dfsg/src/orbit.lua 2013-08-13 08:16:46.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/src/orbit.lua 2014-02-02 09:25:01.000000000 +0000 @@ -1,16 +1,16 @@ +local wsapi = require "wsapi" +local wsreq = require "wsapi.request" +local wsres = require "wsapi.response" +local wsutil = require "wsapi.util" -require "wsapi.request" -require "wsapi.response" -require "wsapi.util" - -local _G, setfenv = _G, setfenv -module("orbit") -local _M = _M -setfenv(1, _G) +local orm +local orpages + +local _M = _M or {} _M._NAME = "orbit" -_M._VERSION = "2.2.0" -_M._COPYRIGHT = "Copyright (C) 2007-2010 Kepler Project" +_M._VERSION = "2.2.1" +_M._COPYRIGHT = "Copyright (C) 2007-2013 Kepler Project" _M._DESCRIPTION = "MVC Web Development for the Kepler platform" local REPARSE = {} @@ -171,7 +171,8 @@ movie = "video/x-sgi-movie", ice = "x-conference/x-cooltalk", rss = "application/rss+xml", - atom = "application/atom+xml" + atom = "application/atom+xml", + json = "application/json" } _M.app_module_methods = {} @@ -379,10 +380,10 @@ function app_module_methods.model(app_module, ...) if app_module.mapper.default then local table_prefix = (app_module._NAME and app_module._NAME .. "_") or "" - if not orbit.model then - require "orbit.model" + if not orm then + orm = require "orbit.model" end - app_module.mapper = orbit.model.new(app_module.mapper.table_prefix or table_prefix, + app_module.mapper = orm.new(app_module.mapper.table_prefix or table_prefix, app_module.mapper.conn, app_module.mapper.driver, app_module.mapper.logging) end return app_module.mapper:new(...) @@ -399,7 +400,7 @@ local prefix = self.prefix or "" local suffix = self.suffix or "" for k, v in pairs(params or {}) do - link[#link + 1] = k .. "=" .. wsapi.util.url_encode(v) + link[#link + 1] = k .. "=" .. wsutil.url_encode(v) end local qs = table.concat(link, "&") if qs and qs ~= "" then @@ -426,8 +427,8 @@ end function web_methods:page(name, env) - if not orbit.pages then - require "orbit.pages" + if not orpages then + orpages = require "orbit.pages" end local filename if name:sub(1, 1) == "/" then @@ -435,19 +436,19 @@ else filename = self.real_path .. "/" .. name end - local template = orbit.pages.load(filename) + local template = orpages.load(filename) if template then - return orbit.pages.fill(self, template, env) + return orpages.fill(self, template, env) end end function web_methods:page_inline(contents, env) - if not orbit.pages then - require "orbit.pages" + if not orpages then + orpages = require "orbit.pages" end - local template = orbit.pages.load(nil, contents) + local template = orpages.load(nil, contents) if template then - return orbit.pages.fill(self, template, env) + return orpages.fill(self, template, env) end end @@ -455,7 +456,7 @@ return self:empty(self.input[param]) end -for name, func in pairs(wsapi.util) do +for name, func in pairs(wsutil) do web_methods[name] = function (self, ...) return func(...) end @@ -477,7 +478,7 @@ if #captures > 0 then for i = 1, #captures do if type(captures[i]) == "string" then - captures[i] = wsapi.util.url_decode(captures[i]) + captures[i] = wsutil.url_decode(captures[i]) end end return item.handler, captures, item.wsapi, index @@ -500,8 +501,8 @@ web.real_path = wsapi_env.APP_PATH end web.doc_root = wsapi_env.DOCUMENT_ROOT - local req = wsapi.request.new(wsapi_env) - local res = wsapi.response.new(web.status, web.headers) + local req = wsreq.new(wsapi_env) + local res = wsres.new(web.status, web.headers) web.set_cookie = function (_, name, value) res:set_cookie(name, value) end @@ -537,9 +538,9 @@ local web, res = make_web_object(app_module, wsapi_env) repeat local reparse = false - local ok, response = xpcall(function () - return handler(web, unpack(captures)) - end, debug.traceback) + local ok, response = xpcall(function () + return handler(web, unpack(captures)) + end, function(msg) return debug.traceback(msg) end) if not ok then res.status = "500 Internal Server Error" res:write(app_module.server_error(web, response)) @@ -563,3 +564,4 @@ end return _M + Binary files /tmp/pE0qlHJ2Sm/lua-orbit-2.2.0+gita6fb46e+dfsg/test/books.db and /tmp/2_uePaVKcF/lua-orbit-2.2.1+dfsg/test/books.db differ diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/test/books.sql lua-orbit-2.2.1+dfsg/test/books.sql --- lua-orbit-2.2.0+gita6fb46e+dfsg/test/books.sql 1970-01-01 00:00:00.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/test/books.sql 2014-02-02 09:25:01.000000000 +0000 @@ -0,0 +1,25 @@ +CREATE TABLE "books" ( + "id" INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, + "title" TEXT, + "author" TEXT, + "year_pub" INTEGER, + "created_at" TEXT, + "updated_at" TEXT +); + + +BEGIN TRANSACTION; +insert into books ("title", "author", "year_pub", "created_at", "updated_at") values ('The secret diaries', 'John Doe', '2010', NULL, NULL); +insert into books ("title", "author", "year_pub", "created_at", "updated_at") values ('Gardens for dry climates', 'Jane Doe', '1999', NULL, NULL); +insert into books ("title", "author", "year_pub", "created_at", "updated_at") values ('DoeScript: a primer', 'O''Reilly', '2013', NULL, NULL); +insert into books ("title", "author", "year_pub", "created_at", "updated_at") values ('There''s no life on Venus', 'Fred Bloggs', '2010', NULL, NULL); +insert into books ("title", "author", "year_pub", "created_at", "updated_at") values ('Letters from Paris', 'Ann Onymous', '2011', NULL, NULL); +insert into books ("title", "author", "year_pub", "created_at", "updated_at") values ('Halfway to nowhere', 'John Doe', '1996', NULL, NULL); +insert into books ("title", "author", "year_pub", "created_at", "updated_at") values ('A test too far', 'John Doe', '1996', NULL, NULL); +insert into books ("title", "author", "year_pub", "created_at", "updated_at") values ('The 1995 diaries', 'John Doe', '1996', NULL, NULL); +insert into books ("title", "author", "year_pub", "created_at", "updated_at") values ('Counting sheep', 'John Doe', '1996', NULL, NULL); +insert into books ("title", "author", "year_pub", "created_at", "updated_at") values ('An estranged husband', 'Jane Doe', '1996', NULL, NULL); +insert into books ("title", "author", "year_pub", "created_at", "updated_at") values ('This year of plenty', 'Fred Bloggs', '1996', NULL, NULL); +insert into books ("title", "author", "year_pub", "created_at", "updated_at") values ('A recursive glance', 'John Doe', '2012', NULL, NULL); +COMMIT; + diff -Nru lua-orbit-2.2.0+gita6fb46e+dfsg/test/test_model.lua lua-orbit-2.2.1+dfsg/test/test_model.lua --- lua-orbit-2.2.0+gita6fb46e+dfsg/test/test_model.lua 1970-01-01 00:00:00.000000000 +0000 +++ lua-orbit-2.2.1+dfsg/test/test_model.lua 2014-02-02 09:25:01.000000000 +0000 @@ -0,0 +1,38 @@ +#! /usr/bin lua +-- Tests the examples from the Orbit reference +local orbit = require "orbit" +local luasql = require "luasql.sqlite3" +module("test_model", package.seeall, orbit.new) + +mapper.conn = luasql.sqlite3():connect("books.db") +mapper.driver = "sqlite3"; mapper.table_prefix = "" +mapper.logging = false +local books = test_model:model "books" + +local res = {} +local count = books:find_all("", { fields = {"count(*)" } })[1]["count(*)"] +print("There are " .. count .. " books in the database.") + +local example = [[ books:find(2) ]] +print("Testing: " .. example) +res = books:find(2) +assert(res.title == "Gardens for dry climates") +print("OK") + +example = [[ books:find_first("author = ? and year_pub > ?", { "John Doe", 1995, order = "year_pub asc" }) ]] +print("Testing: " .. example) +res = books:find_first("author = ? and year_pub > ?", { "John Doe", 1995, order = "year_pub asc" }) +assert(res.title == "Halfway to nowhere") +print("OK") + +example = [[ books:find_all("author = ? and year_pub > ?", { "John Doe", 1995, order = "year_pub asc", count = 5, fields = {"id", "title" } }) ]] +print("Testing: " .. example) +res = books:find_all("author = ? and year_pub > ?", { "John Doe", 1995, order = "year_pub asc", count = 5, fields = {"id", "title" } }) +assert(#res == 5) +print("OK") + +example = [[ books:find_all_by_author_or_author{ "John Doe", "Jane Doe", order = "year_pub asc" } ]] +print("Testing: " .. example) +res = books:find_all_by_author_or_author{ "John Doe", "Jane Doe", order = "year_pub asc" } +assert(#res == 8) +print("OK")