Giter Club home page Giter Club logo

liquex's Introduction

Liquex

A Liquid template parser for Elixir.

Liquid template renderer for Elixir with 100% compatibility with the Liquid gem by Shopify. If you find that this library is not byte for byte equivalent to liquid, please open an issue.

Installation

The package is available in Hex and can be installed by adding liquex to your list of dependencies in mix.exs:

def deps do
  [
    {:liquex, "~> 0.13.0"}
  ]
end

Documentation can be found at https://hexdocs.pm/liquex.

Basic Usage

iex> {:ok, template_ast} = Liquex.parse("Hello {{ name }}!")
iex> {content, _context} = Liquex.render!(template_ast, %{"name" => "World"})

iex> content |> to_string()
"Hello World!"

Supported features

Liquex is byte for byte, 100% compatible with the latest Liquid gem.

Lazy variables

Liquex allows resolver functions for variables that may require some extra work to generate. For example, Shopify has variables for things like available products. Pulling all products every time would be too expensive to do on every render. Instead, it would be better to lazily pull that information as needed.

Instead of adding the product list to the context variable map, you can add a function to the variable map. If a function is accessed in the variable map, it is executed.

products_resolver = fn _parent -> Product.all() end

with {:ok, document} <- Liquex.parse("There are {{ products.size }} products"),
    {result, _} <- Liquex.render!(document, %{products: products_resolver}) do
  result
end

"There are 5 products"

Indifferent access

By default, Liquex accesses your maps and structs that may have atom or string (or other type) keys. Liquex will try a string key first. If that fails, it will fall back to using an atom keys. This is similar to how Ruby on Rails handles many of its hashes.

This allows you to pass in your structs without having to replace all your keys with string keys.

iex> {:ok, template_ast} = Liquex.parse("Hello {{ name }}!")
iex> {content, _context} = Liquex.render!(template_ast, %{name: "World"})
iex> content |> to_string()
"Hello World!"

Caching

Liquex has a built in cache used specifically for the render tag currently. When loading a partial/sub-template using the render tag, it will try pulling from the cache associated with the context.

By default, caching is disabled, but you may use the built in ETS based cache by configuring it in your context.

:ok = Liquex.Cache.SimpleCache.init()
context = Context.new(%{...}, cache: Liquex.Cache.SimpleCache)

The simple cache is by definition quite simple. To use a more complete caching system, such as Cachex, you can create a module that implements the Liquex.Cache behaviour.

The cache system is very early on. It is expected that it will also be used to memoize some of the variables within your context.

Custom filters

Liquex contains the full suite of standard Liquid filters, but you may find that there are still filters that you may want to add.

Liquex supports adding your own custom filters to the render pipeline. When creating the context for the renderer, set the filter module to your own module.

defmodule CustomFilter do
  # Import all the standard liquid filters
  use Liquex.Filter

  def scream(value, _), do: String.upcase(value) <> "!"
end

context = Liquex.Context.new(%{}, filter_module: CustomFilter)
{:ok, template_ast} = Liquex.parse("{{'Hello World' | scream}}"

{result, _} = Liquex.render!(template_ast, context)
result |> to_string()

iex> "HELLO WORLD!"

Custom tags

One of the strong points for Liquex is that the tag parser can be extended to support non-standard tags. For example, Liquid used internally for the Shopify site includes a large range of tags that are not supported by the base Ruby gem. These tags could also be added to Liquex by extending the liquid parser.

defmodule CustomTag do
  @moduledoc false

  @behaviour Liquex.Tag

  import NimbleParsec

  @impl true
  # Parse <<Custom Tag>>
  def parse() do
    text =
      lookahead_not(string(">>"))
      |> utf8_char([])
      |> times(min: 1)
      |> reduce({Kernel, :to_string, []})
      |> tag(:text)

    ignore(string("<<"))
    |> optional(text)
    |> ignore(string(">>"))
  end

  @impl true
  def render(contents, context) do
    {result, context} = Liquex.render!(contents, context)
    {["Custom Tag: ", result], context}
  end
end

defmodule CustomParser do
  use Liquex.Parser, tags: [CustomTag]
end

iex> document = Liquex.parse!("<<Hello World!>>", CustomParser)
iex> {result, _} = Liquex.render!(document, context)
iex> result |> to_string()
"Custom Tag: Hello World!"

Deviations from original Liquid gem

Whitespace is kept in empty blocks

For performance reasons, whitespace is kept within empty blocks such as for/if/unless. The liquid gem checks for "blank" renders and throws them away. Instead, we continue to use IO lists to combine the output and don't check for blank results to avoid too many conversions to strings. Since Liquid is mostly used for whitespace agnostic documents, this seemed like a decent tradeoff. If you need better whitespace control, use {%-, {{-, -%}, and -}}.

liquex's People

Contributors

markglenn avatar jdewar avatar ouven avatar cipater avatar resterle avatar dkulchenko avatar stevencorona avatar tmjoen avatar

Stargazers

 avatar Larry Weya avatar Henricus Louwhoff avatar Eaden McKee avatar Neya avatar John Barker avatar Nicolas Mena avatar Valerii Baleiko avatar Hugo avatar Kyle Johnson avatar bun avatar  avatar Daniel Docki avatar Anton Repushko avatar Vojtěch Hýža avatar  avatar Guillaume avatar Gianni Chiappetta avatar malcolmp avatar Barrie Loydall avatar  avatar Shunji Lin avatar Dung Nguyen avatar Kevin Koltz avatar helioxir avatar Waranyoo Butsingkorn avatar Jordan P.R. avatar peter madsen-mygdal avatar JT A. avatar Pavel Lazureykis avatar Ben Smith avatar Dylan Fareed avatar Thiago Majesk Goulart avatar  avatar Pavel Tsurbeleu avatar Tom Meiselbach avatar Paul Meserve avatar  avatar Roman Heinrich avatar

Watchers

Woohoo avatar  avatar  avatar James Cloos avatar Kevin Koltz avatar  avatar

liquex's Issues

Break up parser over multiple modules

From nimble_parsec's CHANGELOG:

Add defcombinator and support for remote parsec. 
This allows larger parsers to be broken over multiple modules 
to speed up compilation by leveraging parallelism.

Is this something that could be leveraged by Liquex?

Compiling takes quite a bit of time (naturally, since it's a pretty big parser) -- and when I extend base_element to bring in my own custom elements, it will compile liquex first and then my own extended parser afterwards (effectively compiling much of the same code twice). There is maybe no way around this?

Sorry for raising an issue, I was just curious if there was anything to be done here!

Thanks again for a lovely library <3

Return error tuple on failed `render` call.

When calling into Liquex.render/2 with, for example, a bad filter, an error is raised. This forces me to have an error handler by tuple and a try/catch block. Instead, have Liquex.render support returning an error tuple instead and replace the current render/2 function with render!\2.

This is a breaking change, so it might require a rolling out over multiple versions with a deprecation notice.

Current implementation:

  defp assign_preview_content(%{content: content} = assigns) do
    try do
      with {:ok, document} <- Liquex.parse(content || ""),
           {result, _context} <- Liquex.render(document, %{}) do
        assigns
        |> assign(:preview_content, to_string(result))
        |> assign(:error, nil)
        |> assign(:error_line, nil)
      else
        {:error, message, line_no} ->
          assigns
          |> assign(:error, message)
          |> assign(:error_line, line_no)
      end
    rescue
      e in Liquex.Error ->
        assigns
        |> assign(:error, e.message)
        |> assign(:error_line, nil)
    end
  end

Proposed usage:

  defp assign_preview_content(%{content: content} = assigns) do
    with {:ok, document} <- Liquex.parse(content || ""),
         {:ok, result, _context} <- Liquex.render(document, %{}) do
      assigns
      |> assign(:preview_content, to_string(result))
      |> assign(:error, nil)
      |> assign(:error_line, nil)
    else
      {:error, message, line_no} ->
        assigns
        |> assign(:error, message)
        |> assign(:error_line, line_no)
    end
  end

Parser "shortcuts" made private

Hello!

In 0.8.0 a lot of what I call "parser shortcuts" were made private. For instance Liquex.Parser.Object.arguments. I found it helpful to use these in my custom tags, but now that they are private I can't :)

Should I just copy these small functions out to my own code base so you can keep their API private and have more freedom to change them?

Thanks for any advice -- still love the library <3

Make custom tags more developer friendly

Allow a quicker and easier method for creating custom tags within Liquex.

The goal is to allow for the following type of code:

defmodule MyCustomTag do
  import NimbleParsec

  @behaviour Liquex.Tag

  @impl true
  def parse(combinator \\ empty) do
    text =
      lookahead_not(string(">>"))
      |> utf8_char([])
      |> times(min: 1)
      |> reduce({Kernel, :to_string, []})
      |> tag(:text)

    combinator
    |> ignore(string("<<"))
    |> optional(text)
    |> ignore(string(">>"))
  end

  @impl true
  def render(contents, context) do
    {result, context} = Liquex.render(contents, context)
    {["Custom Tag: ", result], context}
  end
end

Then we could generate a parser such as:

defmodule MyParser do
  use Liquex.Parser, tags: [MyCustomTag]
end

and then execute it like this:

{:ok, template} = MyParser.parse("<<Hello World!>>{{ variable }}")

MyParser.render(template, Liquex.Context.new(%{variable: "test"})

This would be a breaking change on the custom tag processor and I'm not sure if what I'm proposing can work the way I have laid out.

Importing sub-template doesn't work in Liquex

I'm using the Liquex version 0.7 template library.

I have the following render function:

def render(template, params) do
    path = "/the/path/to/the/template.html"
    {str, _} = File.read!(path)
    |> Liquex.parse!()
    |> Liquex.render!(params)
    Enum.join(str)
  end

All goes OK if the template if simple:

<center>
  <b>Hello</b> World {{ name }}.
</center>

But if I want to include a sub-template, as for exemple

<center>
  <b>Hello</b> World {{ name }}.
</center>

{% render "footer" %}

(or {% render "footer.html" %} or {% render "footer.liquid" %})

where, footer is

<hr>
This is the footer

I get the following error: ** (Plug.Conn.WrapperError) ** (Liquex.Error) This liquid context does not allow includes.

Pass key to 'resolver functions' and/or structs implementing the Access behavior

Currently, the implementation of lazy variables doesn't allow for the following syntax to be handled lazily:

{{ all_products['wayfarer-shades'].title }}
{% assign article = articles['news/hello-world'] %}

This would be possible through two different approaches; passing the key to the resolver function, or calling Access.fetch/2 on structs that implement Access behavior.

Given the example in the readme, the function would look something like:

products_resolver = fn _parent, key -> Product.get_by(handle: key) end

The Access behavior approach would look something like:

defmodule MyProductResolver do
  @behaviour Access

  defstruct []

  def fetch(_, key) do
    Product.get_by(handle: key) 
  end
end

iex> {:ok, template_ast} = Liquex.parse("Product title: {{ all_products['wayfarer-shades'].title }}")
iex> {content, _context} = Liquex.render(template_ast, %{all_products: % MyProductResolver{}})
iex> content |> to_string()

"Product title: Wayfarer Shades"

After some internal discussions, I've put together a PR (#26) that supports the second approach.

The first approach is likely more ideal, but there were some concerns about arity (ie, an arity 1 function with just the key would be nice, but is already taken) and how changes to that part of the code might interact with your plans for #16. So we decided to not write any code for this without feedback.

If you have any thoughts/feedback about an implementation for the resolver functions, I'd be happy to take a stab at that PR as well. Might even be good to support both use-cases...

Allow filters to pass through for Ecto queryables

The filters currently treat all collections as simple enumerables. However, it would be better if we can pass some of those filters over to code that can optimize SQL queries instead of pulling all records and filtering in code.

For example:

{% assign available_products = products | where: "available" %}

This should allow some piece of the code to pick up this and pass it along to ecto. Recently, Liquex added Liquex.Collection protocol for overriding default behavior for filters. Add in other functions such as sort, sort_natural, and where.

Is there a way to access struct keys in context?

Hi, and first of all thank you so much for a magnificent library!

I was wondering if it is possible to access struct keys?

I added a protocol implementation for Strings.Char/to_string to render the struct if accessed directly, but it would be nice if I could access the keys themselves as well:

person = %Person{location: "Oslo", name: "Blabla"}
{:ok, template_ast} = Liquex.parse("Hello {{ person.location }}!")
context = Liquex.Context.new(%{"person" => person})  
{content, _context} = Liquex.render(template_ast, context)  
{["Hello ", "", "!"],
 %Liquex.Context{
   cycles: %{},
   errors: [],
   filter_module: Liquex.Filter,
   private: %{},
   render_module: nil,
   variables: %{"person" => %Person{location: "Oslo", name: "Blabla"}}
 }}

If I add this to Liquex.Argument module

  defp do_eval(struct, [{:key, key} | tail]) when is_struct(struct) do
    atom_key = safe_to_existing_atom(key)

    struct
    |> Map.get(atom_key)
    |> do_eval(tail)
  end

  defp safe_to_existing_atom(str) do
    try do
      String.to_existing_atom(str)
    rescue
      ArgumentError -> ""
    end
  end  

I can make it work, but I'm not sure if this is an acceptable way of doing things? :)

If you are opposed to this, is there another way I could make this work? Extend the do_eval logic somehow?

Thanks again!

Context variables don't allow question marks at end of name

Liquid allows variables to include a question mark at the end of the name. Liquex currently fails to parse the variable if it contains a special character.

Allow variables to end in a question mark.

{% if form.submitted_successfully? %}
  Success!
{% endif %}

Question about Live View

First of all sorry to ask this here, this library it's just amazing I'm creating a kind of clone of shopify and it would be really nice if I can use this library with LiveView. I'm using LiveView and I'm thinking to create components like header, cart, collections. etc. I wonder if is there any good approach to integrate liveview components with this library of how can I render a liquex template but at the same time have liveview components.

Thanks again for this incredible library

Interested in any of these as PRs?

Hi Mark,

Thanks for the library. It has been really useful and I'm currently using it to drive end-user definable templates in a project of mine. I've developed several large bits of functionality and I'm curious if you'd be interested in PRs to bring any of them into the core library. They are:

  1. An implementation of the Shopify render tag to handle nested rendering (depends on the caller to provide a loader function).

  2. A rendering budget / reduction limit - basically tracks the number of reductions and raises if it's exceeded. Needed this to prevent accidentally creating render tag cycles, but could be applied to the entire render loop.

  3. An implementation of Phoenix.Template.Engine that supports storing the liquid templates in the template folder and parses them at compile time - generating a function per template similar to how eex templates work. In my project, I use this to make the "default" system templates part of my normal workflow, while allowing the end-user to override them with their own liquid (albeit slower because they aren't pre-parsed).

  4. I'm currently using the liquid templates in a LiveView project, and they actually work really well! The only negative is that you lose diff tracking once you enter the liquid template, and also can't render any live_components. The liquid AST is actually conceptually mostly maps to the Phoenix.LiveView.Rendered struct, which is necessary to get full diff tracking, so I'm working on a way to transform the AST.

Would you be interested in having me contributing any of these back? 3 and 4 sorta ride the line of maybe being a separate library.

Allow for cached lazy variables

When accessing a lazy variable/function, allow that function result to be memoized. This would allow quicker access on subsequent accesses.

This could be done by allowing variables and filters to return a tuple of {value, context} or {variable, action} instead of just a value. That way, the function could replace the current variable being accessed either through updating the context or by returning an action to replace the value.

requires elixir version ~> 1.10

Looks like the project elixir version needs to be at least ~> 1.10 due to using is_struct in guards. I hit a compile error trying to use in a project with ~> 1.9

Thanks for creating this package, look forward to using it and swapping out the old Liquid package that had no whitespace control.

Cannot build a tag with several arguments and applied filters

Hi @markglenn

we are trying to bild a tag, that can render an other component, like:

{% render "component_name", binding1: 'foo' %}

This is easy to achieve, thanks to you cool extension mechanism.

problem

But now we wanted the component to be an argument with filters, which cannot be achieved:

{% render "component_name"append: a_variables_content,
    binding1: 'foo' | bar: buzz, 'foo2',
    binding2: 'foo3'
%}

The problem is, that the argument list of a filter can have a keyword list at the end, which makes a string with a colon at the end ambigues (lib/liquex/parser/object.ex:36).

What is this keyword list for filters needed for? I haven't found a test for it nor have I found a shopify liquid example like that?

I'm asking for curiousity. I'm Still a big fan of this project :)

Cheers

Add better error messages on failed parsing

When a parser error occurs, the parser currently returns a message such as: expected end of string at line 3

Instead, we should include some context on what character it failed at. For example if the input text was:

{% form "contact" % }

It would be better to return Expected closing tag at line 3.

parser fails on contains

Hi,

first I want to thank you for this library! Its exzellent extendibility made it to our choice :)

I don't know how to reproduce this in an isolated test but it happens to be, that in some constellations, the parser fails, on the operation "contains".

I found this piece of code at https://github.com/markglenn/liquex/blob/ba9896e24d1177f5b93d97bcb52d751fba9f1a71/lib/liquex/parser/tag/control_flow.ex#L19

 def boolean_expression(combinator \\ empty()) do
    operator =
      choice([
        string("=="),
        string("!="),
        string(">="),
        string("<="),
        string(">"),
        string("<"),
        string("contains")
      ])
      |> map({String, :to_existing_atom, []})

Am I wrong, that this doesn't need to a to_existing_atom, as the choice is already a finit set?

example

{% assign lang = "de,fr" | split: "," %}
{% if lang contains "de" %}
  DE
{% else %}
  OTHER
{% endif %}

Failure

Sorry, I had to x-out parts of the stacktrace. But I guess the point is clear :)

     *****_test.exs:28
     ** (ArgumentError) errors were found at the given arguments:
     
       * 1st argument: invalid UTF8 encoding
     
     stacktrace:
       :erlang.binary_to_existing_atom("contains", :utf8)
       ***** anonymous fn/1 in *****.document__6897/6
       (elixir 1.12.1) lib/enum.ex:1553: Enum."-map/2-lists^map/1-0-"/2
       *****.document__6897/6
       *****.parse__0/6
       *****.parse/2
       (liquex 0.6.0) lib/liquex.ex:178: Liquex.parse/2

I prepared a pr: #13

0.7.1 Access behavior and structs

Hi! It seems that the changes in 0.7.1 wrt Access behaviour and structs broke my tests. I saw in the initial PR there was a check to see if the struct module had implemented the Access behavior, and if not just try to Map.fetch instead. Now it instead raises for me:

** (UndefinedFunctionError) function Brando.Images.Image.fetch/2 is undefined (Brando.Images.Image does not implement the Access behaviour. If you are using get_in/put_in/update_in, you can specify the field to be accessed using Access.key!/1)
     code: assert {:ok, img} = Images.update_image(img, %{title: "Hey"}, user)
     stacktrace:
       (brando 0.52.0-dev) Brando.Images.Image.fetch(%Brando.Images.Image{__meta__: #Ecto.Schema.Metadata<:loaded, "images">, alt: nil, cdn: false, config_target: "default", creator: #Ecto.Association.NotLoaded<association :creator is not loaded>, creator_id: 110, credits: "Credits", deleted_at: nil, dominant_color: nil, focal: nil, formats: [:jpg], height: 292, id: 137, inserted_at: ~N[2022-04-13 13:47:36], path: "image/1.jpg", sizes: %{"large" => "image/large/1.jpg", "medium" => "image/medium/1.jpg", "small" => "image/small/1.jpg", "thumb" => "image/thumb/1.jpg", "xlarge" => "image/xlarge/1.jpg"}, status: nil, title: "Hey", updated_at: ~N[2022-04-13 13:47:36], width: 300}, "id")
       (elixir 1.14.0-dev) lib/access.ex:212: Access.fetch/2
       (liquex 0.7.1) lib/liquex/indifferent.ex:83: Liquex.Indifferent.fetch/2
       (liquex 0.7.1) lib/liquex/indifferent.ex:32: Liquex.Indifferent.get/3
       (liquex 0.7.1) lib/liquex/argument.ex:46: Liquex.Argument.do_eval/3
       (liquex 0.7.1) lib/liquex/render/object.ex:20: Liquex.Render.Object.do_render/2
       (elixir 1.14.0-dev) lib/enum.ex:4182: Enum.find_value_list/3
       (liquex 0.7.1) lib/liquex/render.ex:32: Liquex.Render.render/3
       (brando 0.52.0-dev) lib/brando/blueprint/identifier.ex:17: Brando.Images.Image.__identifier__/1
       (brando 0.52.0-dev) lib/brando/query/mutations.ex:101: Brando.Query.Mutations.update/10
       test/brando/images/images_test.exs:16: (test)

Here it tries to render an identifier for our saved record. This is the liquex template:

"{{ entry.id }}"

Where entry is an %Image{} struct/Ecto schema.

Do we have to implement the Access behavior on everything now, or have I just coasted along on my own broken code for a while now? 😄

Unknown POSIX error while trying to render a template

Hi! I'm trying to render a liquid template using the provided information on the README and I'm getting this error.

Layout.liquid

{% render "post" %}

Post.liquid

Hello From Post

And then:

content = File.read!("theme/layout.liquid")
{:ok, template_ast} = Liquex.parse(content)
{content, _context} = Liquex.render!(template_ast, %{})
** (File.Error) could not  nil: unknown POSIX error
    (liquex 0.10.1) lib/liquex/file_system.ex:22: Liquex.FileSystem.Liquex.BlankFileSystem.read_template_file/2
    (liquex 0.10.1) lib/liquex/tag/render_tag.ex:218: anonymous fn/2 in Liquex.Tag.RenderTag.load_contents/2
    (liquex 0.10.1) lib/liquex/tag/render_tag.ex:143: Liquex.Tag.RenderTag.render/2
    (liquex 0.10.1) lib/liquex/render.ex:25: Liquex.Render.render!/3
    (liquex 0.10.1) lib/liquex.ex:213: Liquex.render!/2
    iex:7: (file)

Do I have to do any other setup for this to work?

{% increment ... %} / {% decrement %} overrides variables defined using assign and capture

increment and decrement tags are overriding variables that are defined by assign and capture. This is because liquex currently has only one level of variable scopes. In the Liquid gem, there are multiple with increment/decrement accessing the base scope while the rest work on nested scopes.

In the example below, a variable named “var” is created using assign. The increment tag is then used several times on a variable with the same name. Note that the increment tag does not affect the value of “var” that was created using assign.

<===> increment.json
{
  "x": 10
}

<===> increment.liquid
{% assign var = 10 %}
{% increment var %}
{% increment var %}
{% increment var %}
{{ var }}
---
{% increment x %}
{% increment x %}
{% increment x %}
{{ x }}

Expected output:

0
1
2
10
---
10
11
12
13

Current output:

11
12
13
13
---
11
12
13
13

Custom protocol for rendering and control flow comparisons

I ran into a little snag with the handling of structs through the String.Chars that I mentioned in #9.

I have two structs: a %Global{} and a %Var{}. Both these have a field called type that could be text/color/boolean/markdown/etc.

Originally I defimpl'ed them with String.Chars:to_string, which works fine for rendering them. The problem appeared when using them in some sort of control flow. I can't compare my %Global{type: :boolean, data: %{value: "false"}} with "false".

If there was a protocol for Liquex it could work:

defprotocol Liquex.Protocol do
  @fallback_to_any true
  @spec render(t) :: any()
  def render(value)
end

defimpl Liquex.Protocol, for: Any do
  def render(value), do: to_string(value)
end

This falls back to the to_string that you use today.

So if I change the left/right logic in Liquex.Expression:

# wrapped `left` and `right` in Protocol
alias Liquex.Protocol

defp do_eval({left, op, right}), do:
    apply(Kernel, op, [Protocol.render(left), Protocol.render(right)])

and call the protocol for object rendering in Liquex.Render.Object:

  def do_render([argument, filters: filters], %Context{} = context) do
    {result, context} =
      argument
      |> List.wrap()
      |> Argument.eval(context)
      |> Filter.apply_filters(filters, context)

    # pass result through protocol
    {Protocol.render(result), context}
  end

then I can have full control of my custom structs if I implement the Liquex.Protocol protocol:

  defimpl Liquex.Protocol, for: Brando.Sites.Global do
    # ...
    def render(%{type: "boolean", data: data}) do
      data
      |> Map.get("value", "false")
      |> to_string()
    end
    # ...
  end

Is this something you'd consider?

If not, sorry for the noise! It's just that this library has solved 99% of my problems and the only one remaining is this 😄

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.