1570419191
Let me show you what we will achieve at the end of this article:
// '@@' makes the function `foo` curried
function @@ foo(a, b, c) {
return a + b + c;
}
console.log(foo(1, 2)(3)); // 6
We are going to create a curry function syntax @@
. The syntax is like the generator function, except you place @@
instead of *
in between the function
keyword and the function name, eg function @@ name(arg1, arg2)
.
In this example, you can have partial application with the function foo
. Calling foo
with the number of parameters less than the arguments required will return a new function of the remaining arguments:
foo(1, 2, 3); // 6
const bar = foo(1, 2); // (n) => 1 + 2 + n
bar(3); // 6
The reason I choose
@@
is that you can’t have@
in a variable name, sofunction@@foo(){}
is still a valid syntax. And the “operator”@
is used for decorator functions but I wanted to use something entirely new, thus@@
.
To achieve this, we are going to:
Sounds impossible 😨?
Don’t worry, I will guide you through every step. Hopefully, at the end of this article, you will be the babel master amongst your peers. 🤠
Head over to babel’s Github repo, click the “Fork” button located at the top left of the page.
If this is your first time forking a popular open-source project, congratulations!
Clone your forked babel to your local workspace and set it up:
$ git clone https://github.com/tanhauhau/babel.git
# set up
$ cd babel
$ make bootstrap
$ make build
Meanwhile, let me briefly walk you through how the babel repository is organised.
Babel uses a monorepo structure, all the packages, eg: @babel/core
, @babel/parser
, @babel/plugin-transform-react-jsx
, etc are in the packages/
folder:
- doc
- packages
- babel-core
- babel-parser
- babel-plugin-transform-react-jsx
- ...
- Gulpfile.js
- Makefile
- ...
Trivia: Babel uses Makefile for automating tasks. For build task, such as
make build
, it will use Gulp as the task runner.
Before we proceed, if you are unfamiliar with parsers and Abstract Syntax Tree (AST), I highly recommend to checkout Vaidehi Joshi’s Leveling Up One’s Parsing Game With ASTs.
To summarise, this is what happened when babel is parsing your code:
string
is a long list of characters: f, u, n, c, t, i, o, n, , @, @, f, ...
function, @@, foo, (, a, ...
If you want to learn more in-depth on compilers in general, Robert Nystrom’s Crafting Interpreters is a gem.
Don’t get scared of by the word compiler, it is nothing but parsing your code and generate XXX out of it. XXX could be machine code, which is the compiler most of us have in mind; XXX could be JavaScript compatible with older browsers, which is the case for Babel.
The folder we are going to work on is packages/babel-parser/
:
- src/
- tokenizer/
- parser/
- plugins/
- jsx/
- typescript/
- flow/
- ...
- test/
We’ve talked about tokenization and parsing, now it’s clear where to find the code for each process. plugins/
folder contains plugins that extend the base parser and add custom syntaxes, such as jsx
and flow
.
Let’s do a Test-driven development (TDD). I find it easier to define the test case then slowly work our way to “fix” it. It is especially true in an unfamiliar codebase, TDD allows you to “easily” point out code places you need to change.
packages/babel-parser/test/curry-function.js
import { parse } from '../lib';
function getParser(code) {
return () => parse(code, { sourceType: 'module' });
}
describe('curry function syntax', function() {
it('should parse', function() {
expect(getParser(`function @@ foo() {}`)()).toMatchSnapshot();
});
});
You can run TEST_ONLY=babel-parser TEST_GREP="curry function" make test-only
to run tests for babel-parser
and see your failing case:
SyntaxError: Unexpected token (1:9)
at Parser.raise (packages/babel-parser/src/parser/location.js:39:63)
at Parser.raise [as unexpected] (packages/babel-parser/src/parser/util.js:133:16)
at Parser.unexpected [as parseIdentifierName] (packages/babel-parser/src/parser/expression.js:2090:18)
at Parser.parseIdentifierName [as parseIdentifier] (packages/babel-parser/src/parser/expression.js:2052:23)
at Parser.parseIdentifier (packages/babel-parser/src/parser/statement.js:1096:52)
If you find scanning through all the test cases takes time, you can directly call
jest
to run the test:
BABEL_ENV=test node_modules/.bin/jest -u packages/babel-parser/test/curry-function.js
Our parser found 2 seemingly innocent @
tokens at a place where they shouldn’t be present.
How do I know that? Let’s start the watch mode, make watch
, wear our detective cap 🕵️ and start digging!
Tracing the stack trace, led us to packages/babel-parser/src/parser/expression.js
where it throws this.unexpected()
.
Let us add some console.log
:
packages/babel-parser/src/parser/expression.js
parseIdentifierName(pos: number, liberal?: boolean): string {
if (this.match(tt.name)) {
// ...
} else {
console.log(this.state.type); // current token
console.log(this.lookahead().type); // next token
throw this.unexpected();
}
}
As you can see, both tokens are @
token:
TokenType {
label: '@',
// ...
}
How do I know this.state.type
and this.lookahead().type
will give me the current and the next token?
Well, I’ll explained them later
Let’s recap what we’ve done so far before we move on:
babel-parser
make test-only
to run the test casemake watch
this.state.type
Here’s what we are going to do next:
If there’s 2 consecutive @
, it should not be separate tokens, it should be a @@
token, the new token we just defined for our curry function
Let’s first look at where a token type is defined: packages/babel-parser/src/tokenizer/types.js.
Here you see a list of tokens, so let’s add our new token definition in as well:
packages/babel-parser/src/tokenizer/types.js
export const types: { [name: string]: TokenType } = {
// ...
at: new TokenType('@'),
atat: new TokenType('@@'),};
Next, let’s find out where the token gets created during tokenization. A quick search on tt.at
within babel-parser/src/tokenizer
lead us to packages/babel-parser/src/tokenizer/index.js
Well, token types are import as
tt
throughout the babel-parser.
Let’s create the token tt.atat
instead of tt.at
if there’s another @
succeed the current @
:
packages/babel-parser/src/tokenizer/index.js
getTokenFromCode(code: number): void {
switch (code) {
// ...
case charCodes.atSign:
// if the next character is a `@` if (this.input.charCodeAt(this.state.pos + 1) === charCodes.atSign) { // create `tt.atat` instead this.finishOp(tt.atat, 2); } else { this.finishOp(tt.at, 1); } return; // ...
}
}
If you run the test again, you will see that the current token and the next token has changed:
// current token
TokenType {
label: '@@',
// ...
}
// next token
TokenType {
label: 'name',
// ...
}
Yeah! It looks good and lets move on.
Before we move on, let’s inspect how generator functions are represented in AST:
As you can see, a generator function is represented by the generator: true
attribute of a FunctionDeclaration
.
Similarly, we can add a curry: true
attribute of the FunctionDeclaration
too if it is a curry function:
We have a plan now, let’s implement it.
A quick search on “FunctionDeclaration” leads us to a function called parseFunction
in packages/babel-parser/src/parser/statement.js, and here we find a line that sets the generator
attribute, let’s add one more line:
packages/babel-parser/src/parser/statement.js
export default class StatementParser extends ExpressionParser {
// ...
parseFunction<T: N.NormalFunction>(
node: T,
statement?: number = FUNC_NO_FLAGS,
isAsync?: boolean = false
): T {
// ...
node.generator = this.eat(tt.star);
node.curry = this.eat(tt.atat); }
}
If you run the test again, you will be amazed that it passed!
PASS packages/babel-parser/test/curry-function.js
curry function syntax
✓ should parse (12ms)
That’s it? How did we miraculously fix it?
I am going to briefly explain how parsing works, and in the process hopefully, you understood what that one-liner change did.
With the list of tokens from the tokenizer, the parser consumes the token one by one and constructs the AST. The parser uses the language grammar specification to decide how to use the tokens, which token to expect next.
The grammar specification looks something like this:
...
ExponentiationExpression -> UnaryExpression
UpdateExpression ** ExponentiationExpression
MultiplicativeExpression -> ExponentiationExpression
MultiplicativeExpression ("*" or "/" or "%") ExponentiationExpression
AdditiveExpression -> MultiplicativeExpression
AdditiveExpression + MultiplicativeExpression
AdditiveExpression - MultiplicativeExpression
...
It explains the precedence of each expressions/statements. For example, an AdditiveExpression
is made up of either:
MultiplicativeExpression
, orAdditiveExpression
followed by +
operator token followed by MultiplicativeExpression
, orAdditiveExpression
followed by -
operator token followed by MultiplicativeExpression
.So if you have an expression 1 + 2 * 3
, it will be like:
(AdditiveExpression "+" 1 (MultiplicativeExpression "*" 2 3))
instead of
(MultiplicativeExpression "*" (AdditiveExpression "+" 1 2) 3)
With these rules, we translate them into parser code:
class Parser {
// ...
parseAdditiveExpression() {
const left = this.parseMultiplicativeExpression();
// if the current token is `+` or `-`
if (this.match(tt.plus) || this.match(tt.minus)) {
const operator = this.state.type;
// move on to the next token
this.nextToken();
const right = this.parseMultiplicativeExpression();
// create the node
this.finishNode(
{
operator,
left,
right,
},
'BinaryExpression'
);
} else {
// return as MultiplicativeExpression
return left;
}
}
}
As you can see here, the parser is recursively in nature, and it goes from the lowest precedence to the highest precedence expressions/statements. Eg: parseAdditiveExpression
calls parseMultiplicativeExpression
, which in turn calls parseExponentiationExpression
, which in turn calls … . This recursive process is called the Recursive Descent Parsing.
If you have noticed, in my examples above, I used some utility function, such as this.eat
, this.match
, this.next
, etc. These are babel parser’s internal functions, yet they are quite ubiquitous amongst parsers as well:
this.match
returns a boolean
indicating whether the current token matches the condition
this.next
moves the token list forward to point to the next token
this.eat
return what this.match
returns and if this.match
returns true
, will do this.next
this.eat
is commonly used for optional operators, like *
in generator function, ;
at the end of statements, and ?
in typescript types.this.lookahead
get the next token without moving forward to make a decision on the current node
If you take a look again the parser code we just changed, it’s easier to read it in now.
packages/babel-parser/src/parser/statement.js
export default class StatementParser extends ExpressionParser {
parseStatementContent(/* ...*/) {
// ...
// NOTE: we call match to check the current token
if (this.match(tt._function)) {
this.next();
// NOTE: function statement has a higher precendence than a generic statement
this.parseFunction();
}
}
// ...
parseFunction(/* ... */) {
// NOTE: we call eat to check whether the optional token exists
node.generator = this.eat(tt.star);
node.curry = this.eat(tt.atat); node.id = this.parseFunctionId();
}
}
I know I didn’t do a good job explaining how a parser works. Here are some resources that I learned from, and I highly recommend them:
Side Note: You might be curious how am I able to visualize the custom syntax in the Babel AST Explorer, where I showed you the new “curry” attribute in the AST.
That’s because I’ve added a new feature in the Babel AST Explorer where you can upload your custom parser!
If you go to packages/babel-parser/lib
, you would find the compiled version of your parser and the source map. Open the drawer of the Babel AST Explorer, you will see a button to upload a custom parser. Drag the packages/babel-parser/lib/index.js
in and you will be visualizing the AST generated via your custom parser!
With our custom babel parser done, let’s move on to write our babel plugin.
But maybe before that, you may have some doubts on how are we going to use our custom babel parser, especially with whatever build stack we are using right now?
Well, fret not. A babel plugin can provide a custom parser, which is documented on the babel website
babel-plugin-transformation-curry-function.js
import customParser from './custom-parser';
export default function ourBabelPlugin() {
return {
parserOverride(code, opts) {
return customParser.parse(code, opts);
},
};
}
Since we forked out the babel parser, all existing babel parser options or built-in plugins will still work perfectly.
With this doubt out of the way, let see how we can make our curry function curryable? (not entirely sure there’s such word)
Before we start, if you have eagerly tried to add our plugin into your build system, you would notice that the curry function gets compiled to a normal function.
This is because, after parsing + transformation, babel will use @babel/generator to generate code from the transformed AST. Since the @babel/generator
has no idea about the new curry
attribute we added, it will be omitted.
If one day curry function becomes the new JavaScript syntax, you may want to make a pull request to add one more line here!
Ok, to make our function curryable, we can wrap it with a currying
helper higher-order function:
function currying(fn) {
const numParamsRequired = fn.length;
function curryFactory(params) {
return function (...args) {
const newParams = params.concat(args);
if (newParams.length >= numParamsRequired) {
return fn(...newParams);
}
return curryFactory(newParams);
}
}
return curryFactory([]);
}
If you want to learn how to write a currying function, you can read this Currying in JS
So when we transform our curry function, we can transform it into the following:
// from
function @@ foo(a, b, c) {
return a + b + c;
}
// to
const foo = currying(function foo(a, b, c) {
return a + b + c;
})
Let’s first ignore function hoisting in JavaScript, where you can call
foo
before it is defined.
If you have read my step-by-step guide on babel transformation, writing this transformation should be manageable:
babel-plugin-transformation-curry-function.js
export default function ourBabelPlugin() {
return {
// ...
visitor: { FunctionDeclaration(path) { if (path.get('curry').node) { // const foo = curry(function () { ... }); path.node.curry = false; path.replaceWith( t.variableDeclaration('const', [ t.variableDeclarator( t.identifier(path.get('id.name').node), t.callExpression(t.identifier('currying'), [ t.toExpression(path.node), ]) ), ]) ); } }, }, };
}
The question is how do we provide the currying
function?
There are 2 ways:
currying
has been declared in the global scope.Basically, your job is done here.
If currying
is not defined, then when executing the compiled code, the runtime will scream out “currying is not defined”, just like the “regeneratorRuntime is not defined”.
So probably you have to educate the users to install currying
polyfills in order to use your babel-plugin-transformation-curry-function
.
@babel/helpers
You can add a new helper to @babel/helpers
, which of course you are unlikely to merge that into the official @babel/helpers
, so you would have to figure a way to make @babel/core
to resolve to your @babel/helpers
:
package.json
{
"resolutions": {
"@babel/helpers": "7.6.0--your-custom-forked-version",
}
}
Adding a new helper function into @babel/helpers
is very easy.
Head over to packages/babel-helpers/src/helpers.js and add a new entry:
helpers.currying = helper("7.6.0")`
export default function currying(fn) {
const numParamsRequired = fn.length;
function curryFactory(params) {
return function (...args) {
const newParams = params.concat(args);
if (newParams.length >= numParamsRequired) {
return fn(...newParams);
}
return curryFactory(newParams);
}
}
return curryFactory([]);
}
`;
The helper tag function specifies the @babel/core
version required. The trick here is to export default
the currying
function.
To use the helper, just call the this.addHelper()
:
// ...
path.replaceWith(
t.variableDeclaration('const', [
t.variableDeclarator(
t.identifier(path.get('id.name').node),
t.callExpression(this.addHelper("currying"), [
t.toExpression(path.node),
])
),
])
);
The this.addHelper
will inject the helper at the top of the file if needed, and returns an Identifier
to the injected function.
We’ve seen how we can modify the babel parser function, write our own babel transform plugin (which was brief mainly because I have a detailed cover in previous post, a brief touch on @babel/generator
and also how we can add helper functions via @babel/helpers
.
Along the way, we had a crash course on how a parser works, which I will provide the links to further reading at the bottom.
The steps we’ve gone through above is similar to part of the TC39 proposal process when defining a new JavaScript specification. When proposing a new specification, the champion of the proposal usually write polyfills or forked out babel to write proof-of-concept demos. As you’ve seen, forking a parser or writing polyfills is not the hardest part of the process, but to define the problem space, plan and think through the use cases and edge cases, and gather opinions and suggestions from the community. To this end, I am grateful to the proposal champion, for their effort in pushing the JavaScript language forward.
Finally, if you want to see the code we’ve done so far in a full picture, you can check it out from Github.
#javascript
1655630160
Install via pip:
$ pip install pytumblr
Install from source:
$ git clone https://github.com/tumblr/pytumblr.git
$ cd pytumblr
$ python setup.py install
A pytumblr.TumblrRestClient
is the object you'll make all of your calls to the Tumblr API through. Creating one is this easy:
client = pytumblr.TumblrRestClient(
'<consumer_key>',
'<consumer_secret>',
'<oauth_token>',
'<oauth_secret>',
)
client.info() # Grabs the current user information
Two easy ways to get your credentials to are:
interactive_console.py
tool (if you already have a consumer key & secret)client.info() # get information about the authenticating user
client.dashboard() # get the dashboard for the authenticating user
client.likes() # get the likes for the authenticating user
client.following() # get the blogs followed by the authenticating user
client.follow('codingjester.tumblr.com') # follow a blog
client.unfollow('codingjester.tumblr.com') # unfollow a blog
client.like(id, reblogkey) # like a post
client.unlike(id, reblogkey) # unlike a post
client.blog_info(blogName) # get information about a blog
client.posts(blogName, **params) # get posts for a blog
client.avatar(blogName) # get the avatar for a blog
client.blog_likes(blogName) # get the likes on a blog
client.followers(blogName) # get the followers of a blog
client.blog_following(blogName) # get the publicly exposed blogs that [blogName] follows
client.queue(blogName) # get the queue for a given blog
client.submission(blogName) # get the submissions for a given blog
Creating posts
PyTumblr lets you create all of the various types that Tumblr supports. When using these types there are a few defaults that are able to be used with any post type.
The default supported types are described below.
We'll show examples throughout of these default examples while showcasing all the specific post types.
Creating a photo post
Creating a photo post supports a bunch of different options plus the described default options * caption - a string, the user supplied caption * link - a string, the "click-through" url for the photo * source - a string, the url for the photo you want to use (use this or the data parameter) * data - a list or string, a list of filepaths or a single file path for multipart file upload
#Creates a photo post using a source URL
client.create_photo(blogName, state="published", tags=["testing", "ok"],
source="https://68.media.tumblr.com/b965fbb2e501610a29d80ffb6fb3e1ad/tumblr_n55vdeTse11rn1906o1_500.jpg")
#Creates a photo post using a local filepath
client.create_photo(blogName, state="queue", tags=["testing", "ok"],
tweet="Woah this is an incredible sweet post [URL]",
data="/Users/johnb/path/to/my/image.jpg")
#Creates a photoset post using several local filepaths
client.create_photo(blogName, state="draft", tags=["jb is cool"], format="markdown",
data=["/Users/johnb/path/to/my/image.jpg", "/Users/johnb/Pictures/kittens.jpg"],
caption="## Mega sweet kittens")
Creating a text post
Creating a text post supports the same options as default and just a two other parameters * title - a string, the optional title for the post. Supports markdown or html * body - a string, the body of the of the post. Supports markdown or html
#Creating a text post
client.create_text(blogName, state="published", slug="testing-text-posts", title="Testing", body="testing1 2 3 4")
Creating a quote post
Creating a quote post supports the same options as default and two other parameter * quote - a string, the full text of the qote. Supports markdown or html * source - a string, the cited source. HTML supported
#Creating a quote post
client.create_quote(blogName, state="queue", quote="I am the Walrus", source="Ringo")
Creating a link post
#Create a link post
client.create_link(blogName, title="I like to search things, you should too.", url="https://duckduckgo.com",
description="Search is pretty cool when a duck does it.")
Creating a chat post
Creating a chat post supports the same options as default and two other parameters * title - a string, the title of the chat post * conversation - a string, the text of the conversation/chat, with diablog labels (no html)
#Create a chat post
chat = """John: Testing can be fun!
Renee: Testing is tedious and so are you.
John: Aw.
"""
client.create_chat(blogName, title="Renee just doesn't understand.", conversation=chat, tags=["renee", "testing"])
Creating an audio post
Creating an audio post allows for all default options and a has 3 other parameters. The only thing to keep in mind while dealing with audio posts is to make sure that you use the external_url parameter or data. You cannot use both at the same time. * caption - a string, the caption for your post * external_url - a string, the url of the site that hosts the audio file * data - a string, the filepath of the audio file you want to upload to Tumblr
#Creating an audio file
client.create_audio(blogName, caption="Rock out.", data="/Users/johnb/Music/my/new/sweet/album.mp3")
#lets use soundcloud!
client.create_audio(blogName, caption="Mega rock out.", external_url="https://soundcloud.com/skrillex/sets/recess")
Creating a video post
Creating a video post allows for all default options and has three other options. Like the other post types, it has some restrictions. You cannot use the embed and data parameters at the same time. * caption - a string, the caption for your post * embed - a string, the HTML embed code for the video * data - a string, the path of the file you want to upload
#Creating an upload from YouTube
client.create_video(blogName, caption="Jon Snow. Mega ridiculous sword.",
embed="http://www.youtube.com/watch?v=40pUYLacrj4")
#Creating a video post from local file
client.create_video(blogName, caption="testing", data="/Users/johnb/testing/ok/blah.mov")
Editing a post
Updating a post requires you knowing what type a post you're updating. You'll be able to supply to the post any of the options given above for updates.
client.edit_post(blogName, id=post_id, type="text", title="Updated")
client.edit_post(blogName, id=post_id, type="photo", data="/Users/johnb/mega/awesome.jpg")
Reblogging a Post
Reblogging a post just requires knowing the post id and the reblog key, which is supplied in the JSON of any post object.
client.reblog(blogName, id=125356, reblog_key="reblog_key")
Deleting a post
Deleting just requires that you own the post and have the post id
client.delete_post(blogName, 123456) # Deletes your post :(
A note on tags: When passing tags, as params, please pass them as a list (not a comma-separated string):
client.create_text(blogName, tags=['hello', 'world'], ...)
Getting notes for a post
In order to get the notes for a post, you need to have the post id and the blog that it is on.
data = client.notes(blogName, id='123456')
The results include a timestamp you can use to make future calls.
data = client.notes(blogName, id='123456', before_timestamp=data["_links"]["next"]["query_params"]["before_timestamp"])
# get posts with a given tag
client.tagged(tag, **params)
This client comes with a nice interactive console to run you through the OAuth process, grab your tokens (and store them for future use).
You'll need pyyaml
installed to run it, but then it's just:
$ python interactive-console.py
and away you go! Tokens are stored in ~/.tumblr
and are also shared by other Tumblr API clients like the Ruby client.
The tests (and coverage reports) are run with nose, like this:
python setup.py test
Author: tumblr
Source Code: https://github.com/tumblr/pytumblr
License: Apache-2.0 license
1622207074
Who invented JavaScript, how it works, as we have given information about Programming language in our previous article ( What is PHP ), but today we will talk about what is JavaScript, why JavaScript is used The Answers to all such questions and much other information about JavaScript, you are going to get here today. Hope this information will work for you.
JavaScript language was invented by Brendan Eich in 1995. JavaScript is inspired by Java Programming Language. The first name of JavaScript was Mocha which was named by Marc Andreessen, Marc Andreessen is the founder of Netscape and in the same year Mocha was renamed LiveScript, and later in December 1995, it was renamed JavaScript which is still in trend.
JavaScript is a client-side scripting language used with HTML (Hypertext Markup Language). JavaScript is an Interpreted / Oriented language called JS in programming language JavaScript code can be run on any normal web browser. To run the code of JavaScript, we have to enable JavaScript of Web Browser. But some web browsers already have JavaScript enabled.
Today almost all websites are using it as web technology, mind is that there is maximum scope in JavaScript in the coming time, so if you want to become a programmer, then you can be very beneficial to learn JavaScript.
In JavaScript, ‘document.write‘ is used to represent a string on a browser.
<script type="text/javascript">
document.write("Hello World!");
</script>
<script type="text/javascript">
//single line comment
/* document.write("Hello"); */
</script>
#javascript #javascript code #javascript hello world #what is javascript #who invented javascript
1606912089
#how to build a simple calculator in javascript #how to create simple calculator using javascript #javascript calculator tutorial #javascript birthday calculator #calculator using javascript and html
1616670795
It is said that a digital resource a business has must be interactive in nature, so the website or the business app should be interactive. How do you make the app interactive? With the use of JavaScript.
Does your business need an interactive website or app?
Hire Dedicated JavaScript Developer from WebClues Infotech as the developer we offer is highly skilled and expert in what they do. Our developers are collaborative in nature and work with complete transparency with the customers.
The technology used to develop the overall app by the developers from WebClues Infotech is at par with the latest available technology.
Get your business app with JavaScript
For more inquiry click here https://bit.ly/31eZyDZ
Book Free Interview: https://bit.ly/3dDShFg
#hire dedicated javascript developers #hire javascript developers #top javascript developers for hire #hire javascript developer #hire a freelancer for javascript developer #hire the best javascript developers
1623389100
Today, We will see laravel 8 create custom helper function example, as we all know laravel provides many ready mate function in their framework, but many times we need to require our own customized function to use in our project that time we need to create custom helper function, So, here i am show you custom helper function example in laravel 8.
#laravel 8 create custom helper function example #laravel #custom helper function #how to create custom helper in laravel 8 #laravel helper functions #custom helper functions in laravel